├── Sources ├── KeyValueStorageLegacy │ ├── Extensions │ │ └── Optional+Extensions.swift │ ├── KeyValueStorageType.swift │ ├── KeyValueStorageKey.swift │ ├── KeyValueStorage.swift │ └── Helpers │ │ └── KeychainHelper.swift ├── KeyValueStorage │ ├── Coders │ │ ├── DataCoder.swift │ │ ├── JSONDataCoder.swift │ │ └── XMLDataCoder.swift │ ├── Helpers │ │ ├── SendableConformances.swift │ │ ├── Typealiases.swift │ │ └── KeychainWrapper.swift │ ├── Resources │ │ └── PrivacyInfo.xcprivacy │ ├── Storages │ │ ├── Layers │ │ │ ├── KeyValueDataStorage.swift │ │ │ ├── KeyValueCodingStorage.swift │ │ │ └── KeyValueObservableStorage.swift │ │ ├── InMemoryStorage.swift │ │ ├── UserDefaultsStorage.swift │ │ ├── KeychainStorage.swift │ │ └── FileStorage.swift │ └── UnifiedStorage.swift ├── KeyValueStorageLegacySwiftUI │ └── KeyValueStoragePropertyWrapper+SwiftUI.swift └── KeyValueStorageLegacyWrapper │ └── KeyValueStoragePropertyWrapper.swift ├── .github └── workflows │ └── swift.yml ├── Tests ├── KeyValueStorageLegacyTests │ ├── OptionalExtensionTests.swift │ ├── ThreadSafetyTests.swift │ ├── KeychainHelperTests.swift │ ├── KeyValueStoragePropertyWrapperTests.swift │ └── KeyValueStorageTests.swift └── KeyValueStorageTests │ ├── Mocks │ ├── UserDefaultsMock.swift │ ├── InMemoryMock.swift │ ├── FileManagerMock.swift │ └── KeychainHelperMock.swift │ ├── DataCoders │ └── DataCodersTests.swift │ ├── KeyValueObservableStorageTests.swift │ ├── KeyValueCodingStorageTests.swift │ ├── UnifiedStorageTests.swift │ └── DataStorageTests │ ├── InMemoryStorageTests.swift │ ├── UserDefaultsStorageTests.swift │ └── FileStorageTests.swift ├── KeyValueStorageSwift.podspec ├── LICENSE ├── Package.swift ├── .gitignore └── README.md /Sources/KeyValueStorageLegacy/Extensions/Optional+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Optional+Extensions.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/27/22. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Optional { 11 | func unwrapped(_ defaultValue: Wrapped) -> Wrapped { 12 | self ?? defaultValue 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Coders/DataCoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataCoder.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 31.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol DataCoder: Sendable { 11 | func encode(_ value: Value) async throws -> Data 12 | func decode(_ data: Data) async throws -> Value 13 | } 14 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Helpers/SendableConformances.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SendableConformances.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 31.12.23. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | extension UserDefaults: @unchecked Sendable { } 12 | extension AnyPublisher: @unchecked Sendable { } 13 | 14 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) 15 | extension AsyncPublisher: @unchecked Sendable { } 16 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Helpers/Typealiases.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Typealiases.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 29.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public typealias CodingValue = Codable & Sendable 11 | public typealias UserDefaultsKey = UnifiedStorageKey 12 | public typealias KeychainKey = UnifiedStorageKey 13 | public typealias InMemoryKey = UnifiedStorageKey 14 | public typealias FileKey = UnifiedStorageKey 15 | 16 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | NSPrivacyAccessedAPIType 15 | NSPrivacyAccessedAPICategoryUserDefaults 16 | NSPrivacyAccessedAPITypeReasons 17 | 18 | C56D.1 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Coders/JSONDataCoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONDataCoder.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 31.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public actor JSONDataCoder: DataCoder { 11 | private let decoder: JSONDecoder 12 | private let encoder: JSONEncoder 13 | 14 | public init(decoder: JSONDecoder = .init(), encoder: JSONEncoder = .init()) { 15 | self.decoder = decoder 16 | self.encoder = encoder 17 | } 18 | 19 | public func encode(_ value: Value) throws -> Data { 20 | try encoder.encode(value) 21 | } 22 | 23 | public func decode(_ data: Data) throws -> Value { 24 | try decoder.decode(Value.self, from: data) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Build & Test 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [macos-14] 14 | swift: ["5.10"] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: swift-actions/setup-swift@v2 18 | name: Set up Swift 19 | with: 20 | swift-version: ${{ matrix.swift }} 21 | - name: Get Swift version 22 | run: swift --version 23 | - uses: actions/checkout@v4 24 | name: Checkout 25 | - name: Build 26 | run: swift build -v 27 | - name: Test 28 | run: swift test -v --enable-code-coverage 29 | - name: Upload to Codecov 30 | uses: codecov/codecov-action@v3 31 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Coders/XMLDataCoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XMLDataCoder.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 31.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | public actor XMLDataCoder: DataCoder { 11 | private let decoder: PropertyListDecoder 12 | private let encoder: PropertyListEncoder 13 | 14 | public init(decoder: PropertyListDecoder = .init(), encoder: PropertyListEncoder = .init()) { 15 | self.decoder = decoder 16 | self.encoder = encoder 17 | } 18 | 19 | public func encode(_ value: Value) throws -> Data { 20 | try encoder.encode(value) 21 | } 22 | 23 | public func decode(_ data: Data) throws -> Value { 24 | try decoder.decode(Value.self, from: data) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageLegacyTests/OptionalExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OptionalExtensionTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/27/22. 6 | // 7 | 8 | import XCTest 9 | @testable import KeyValueStorageLegacy 10 | 11 | class OptionalExtensionTests: XCTestCase { 12 | 13 | func testSome() { 14 | // Given 15 | let int: Int? = 45 16 | 17 | // When 18 | let unwrapped = int.unwrapped(88) 19 | 20 | // Then 21 | XCTAssertEqual(unwrapped, 45) 22 | } 23 | 24 | func testNil() { 25 | // Given 26 | let int: Int? = nil 27 | 28 | // When 29 | let unwrapped = int.unwrapped(88) 30 | 31 | // Then 32 | XCTAssertEqual(unwrapped, 88) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/Mocks/UserDefaultsMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsMock.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 02.03.24. 6 | // 7 | 8 | import Foundation 9 | @testable import KeyValueStorage 10 | 11 | final class UserDefaultsMock: UserDefaults { 12 | var storage = [String: Data]() 13 | override func data(forKey defaultName: String) -> Data? { 14 | storage[defaultName] 15 | } 16 | 17 | override func set(_ value: Any?, forKey defaultName: String) { 18 | storage[defaultName] = value as? Data 19 | } 20 | 21 | override func removeObject(forKey defaultName: String) { 22 | storage[defaultName] = nil 23 | } 24 | 25 | override func removePersistentDomain(forName domainName: String) { 26 | storage.removeAll() 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Sources/KeyValueStorageLegacy/KeyValueStorageType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueStorageType.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/27/22. 6 | // 7 | 8 | /// This enum contains all the supported storage types 9 | public enum KeyValueStorageType: Hashable { 10 | 11 | /// This storage type persists only within an app session. 12 | case inMemory 13 | 14 | /// This storage type persists within the whole app lifetime. 15 | case userDefaults 16 | 17 | /// This storage type keeps the items in a secure storage and persists even app re-installations. 18 | /// - parameter accessibility: Accessibility to use when retrieving the keychain item. 19 | /// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. 20 | case keychain(accessibility: KeychainAccessibility = .whenUnlocked, isSynchronizable: Bool = false) 21 | } 22 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/Mocks/InMemoryMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 02.03.24. 6 | // 7 | 8 | import Foundation 9 | @testable import KeyValueStorage 10 | 11 | final class InMemoryMock: InMemoryStorage { 12 | private(set) var saveCalled = false 13 | private(set) var fetchCalled = false 14 | private(set) var deleteCalled = false 15 | private(set) var clearCalled = false 16 | 17 | override func save(_ value: Data, forKey key: InMemoryStorage.Key) { 18 | saveCalled = true 19 | super.save(value, forKey: key) 20 | } 21 | 22 | override func fetch(forKey key: InMemoryStorage.Key) -> Data? { 23 | fetchCalled = true 24 | return super.fetch(forKey: key) 25 | } 26 | 27 | override func delete(forKey key: InMemoryStorage.Key) { 28 | deleteCalled = true 29 | super.delete(forKey: key) 30 | } 31 | 32 | override func clear() { 33 | clearCalled = true 34 | super.clear() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /KeyValueStorageSwift.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = "KeyValueStorageSwift" 3 | spec.version = "2.1.0" 4 | 5 | spec.summary = "Key-value storage written in Swift." 6 | spec.description = "An elegant, multipurpose key-value storage, compatible with all Apple platforms." 7 | spec.homepage = "https://github.com/narek-sv/KeyValueStorage" 8 | spec.license = { :type => "MIT", :file => "LICENSE" } 9 | spec.author = { "Narek Sahakyan" => "narek.sv.work@gmail.com" } 10 | 11 | spec.swift_version = "5.10" 12 | spec.ios.deployment_target = "13.0" 13 | spec.osx.deployment_target = "10.15" 14 | spec.watchos.deployment_target = "6.0" 15 | spec.tvos.deployment_target = "13.0" 16 | 17 | spec.source = { :git => "https://github.com/narek-sv/KeyValueStorage.git", :tag => "v2.1.0" } 18 | spec.source_files = "Sources/KeyValueStorage/**/*" 19 | 20 | spec.resource_bundles = {"KeyValueStorageSwift" => ["Sources/KeyValueStorage/Resources/PrivacyInfo.xcprivacy"]} 21 | 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 narek-sv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/Mocks/FileManagerMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileManagerMock.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 26.02.24. 6 | // 7 | 8 | import Foundation 9 | 10 | final class FileManagerMock: FileManager { 11 | var storage = [String: Data]() 12 | var removeItemError: CocoaError? 13 | var createDirectoryError: CocoaError? 14 | var createFileError: CocoaError? 15 | 16 | override func removeItem(atPath path: String) throws { 17 | if let removeItemError { 18 | throw removeItemError 19 | } 20 | 21 | storage[path] = nil 22 | } 23 | 24 | override func createDirectory(atPath path: String, withIntermediateDirectories createIntermediates: Bool, attributes: [FileAttributeKey : Any]? = nil) throws { 25 | if let createDirectoryError { 26 | throw createDirectoryError 27 | } 28 | } 29 | 30 | override func createFile(atPath path: String, contents data: Data?, attributes attr: [FileAttributeKey : Any]? = nil) -> Bool { 31 | storage[path] = data 32 | return createFileError == nil 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Storages/Layers/KeyValueDataStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueDataStorage.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 11.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Data Storage Protocol 11 | 12 | public protocol KeyValueDataStorage: Sendable { 13 | associatedtype Key: KeyValueDataStorageKey 14 | associatedtype Domain: KeyValueDataStorageDomain 15 | associatedtype Error: KeyValueDataStorageError 16 | 17 | static var defaultGroup: String { get } 18 | 19 | var domain: Domain? { get } 20 | 21 | init() async throws 22 | init(domain: Domain) async throws 23 | 24 | func fetch(forKey key: Key) async throws -> Data? 25 | func save(_ value: Data, forKey key: Key) async throws 26 | func set(_ value: Data?, forKey key: Key) async throws 27 | func delete(forKey key: Key) async throws 28 | func clear() async throws 29 | } 30 | 31 | // MARK: - Default Implementations 32 | 33 | public extension KeyValueDataStorage { 34 | static var defaultGroup: String { 35 | Bundle.main.bundleIdentifier ?? "KeyValueDataStorage" 36 | } 37 | } 38 | 39 | // MARK: - Associated Type Requirements 40 | 41 | public typealias KeyValueDataStorageKey = Hashable & Sendable 42 | public typealias KeyValueDataStorageDomain = Hashable & Sendable 43 | public typealias KeyValueDataStorageError = Error & Sendable 44 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/Mocks/KeychainHelperMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainMock.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 26.02.24. 6 | // 7 | 8 | import Foundation 9 | @testable import KeyValueStorage 10 | 11 | final class KeychainHelperMock: KeychainWrapper { 12 | var storage = [String: Data]() 13 | var getError: Error? 14 | var setError: KeychainWrapperError? 15 | var removeError: KeychainWrapperError? 16 | var removeAllError: KeychainWrapperError? 17 | 18 | override func get(forKey key: String, withAccessibility accessibility: KeychainAccessibility? = nil, isSynchronizable: Bool = false) throws -> Data? { 19 | if let getError { 20 | throw getError 21 | } 22 | 23 | return storage[key] 24 | } 25 | 26 | override func set(_ value: Data, forKey key: String, withAccessibility accessibility: KeychainAccessibility? = nil, isSynchronizable: Bool = false) throws { 27 | if let setError { 28 | throw setError 29 | } 30 | 31 | return storage[key] = value 32 | } 33 | 34 | override func remove(forKey key: String, withAccessibility accessibility: KeychainAccessibility? = nil, isSynchronizable: Bool = false) throws { 35 | if let removeError { 36 | throw removeError 37 | } 38 | 39 | storage[key] = nil 40 | } 41 | 42 | override func removeAll() throws { 43 | if let removeAllError { 44 | throw removeAllError 45 | } 46 | 47 | storage.removeAll() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/KeyValueStorageLegacySwiftUI/KeyValueStoragePropertyWrapper+SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueStoragePropertyWrapper+SwiftUI.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 10.12.23. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | import KeyValueStorageLegacy 11 | import KeyValueStorageLegacyWrapper 12 | 13 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 14 | @propertyWrapper 15 | public struct ObservedStorage: DynamicProperty { 16 | @ObservedObject private var updateTrigger = KeyValueStorageUpdateTrigger() 17 | private var underlyingStorage: Storage 18 | 19 | public var wrappedValue: Value? { 20 | get { 21 | underlyingStorage.wrappedValue 22 | } 23 | 24 | nonmutating set { 25 | underlyingStorage.wrappedValue = newValue 26 | } 27 | } 28 | 29 | public var projectedValue: Binding { 30 | .init( 31 | get: { wrappedValue }, 32 | set: { wrappedValue = $0 } 33 | ) 34 | } 35 | 36 | public init(key: KeyValueStorageKey, storage: KeyValueStorage = .default) { 37 | self.underlyingStorage = .init(key: key, storage: storage) 38 | self.updateTrigger.subscribtion = underlyingStorage.publisher.sink { [weak updateTrigger] _ in 39 | updateTrigger?.value.toggle() 40 | } 41 | } 42 | 43 | private final class KeyValueStorageUpdateTrigger: ObservableObject { 44 | var subscribtion: AnyCancellable? 45 | @Published var value = false 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/KeyValueStorageLegacy/KeyValueStorageKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueStorageKey.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/27/22. 6 | // 7 | 8 | /// This struct is intended to uniquely identify the item value, type, and all the necessary info about the storage. 9 | public struct KeyValueStorageKey { 10 | 11 | /// `name` is used to uniquely identify the item. 12 | public let name: String 13 | 14 | /// `codingType` is used for properly encoding and decoding the item. 15 | public let codingType: T.Type 16 | 17 | /// `storageType` is used for specifing the storage type where the item will be kept. 18 | public let storageType: KeyValueStorageType 19 | 20 | /// Initializes the key by specifying the key name and the storage type. 21 | /// - parameter name: The name of the key. 22 | /// - parameter storage: The storage type. Default value is `userDefaults`. 23 | public init(name: String, storage: KeyValueStorageType = .userDefaults) { 24 | self.name = name 25 | self.codingType = T.self 26 | self.storageType = storage 27 | } 28 | } 29 | 30 | extension KeyValueStorageKey: Hashable { 31 | public static func == (lhs: KeyValueStorageKey, rhs: KeyValueStorageKey) -> Bool { 32 | lhs.name == rhs.name && 33 | lhs.storageType == rhs.storageType && 34 | lhs.codingType == rhs.codingType 35 | 36 | } 37 | 38 | public func hash(into hasher: inout Hasher) { 39 | hasher.combine(name) 40 | hasher.combine(storageType) 41 | hasher.combine(String(describing: codingType)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/DataCoders/DataCodersTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataCodersTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 02.03.24. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import KeyValueStorage 11 | 12 | final class DataCodersTests: XCTestCase { 13 | func testJSONCoding() async throws { 14 | // Given 15 | let coder = JSONDataCoder() 16 | let codable1 = "rootObject" 17 | let codable2 = ["rootObject"] 18 | 19 | // When 20 | let encoded1 = try await coder.encode(codable1) 21 | let encoded2 = try await coder.encode(codable2) 22 | 23 | // Then 24 | XCTAssertFalse(encoded1.isEmpty) 25 | XCTAssertFalse(encoded2.isEmpty) 26 | 27 | // When 28 | let decoded1 = try await coder.decode(encoded1) as String 29 | let decoded2 = try await coder.decode(encoded2) as [String] 30 | 31 | // Then 32 | XCTAssertEqual(decoded1, codable1) 33 | XCTAssertEqual(decoded2, codable2) 34 | 35 | 36 | } 37 | 38 | func testXMLCoding() async throws { 39 | // Given 40 | let coder = XMLDataCoder() 41 | let codable = ["rootObject"] 42 | 43 | // When 44 | let encoded = try await coder.encode(codable) 45 | 46 | // Then 47 | XCTAssertFalse(encoded.isEmpty) 48 | 49 | // When 50 | let decoded = try await coder.decode(encoded) as [String] 51 | 52 | // Then 53 | XCTAssertEqual(decoded, codable) 54 | 55 | // When - Then 56 | do { 57 | _ = try await coder.encode("rootObject") 58 | XCTFail("XML parser cant decode root objects") 59 | } catch { } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageLegacyTests/ThreadSafetyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThreadSafetyTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/28/22. 6 | // 7 | 8 | import XCTest 9 | @testable import KeyValueStorageLegacy 10 | 11 | class ThreadSafetyTests: XCTestCase { 12 | private var storage: KeyValueStorage! 13 | 14 | override func setUp() { 15 | storage = KeyValueStorage() 16 | } 17 | 18 | override func tearDown() { 19 | storage.clear() 20 | } 21 | 22 | func testSafety() throws { 23 | stressTest(in: .inMemory) 24 | stressTest(in: .userDefaults) 25 | stressTest(in: .keychain()) 26 | } 27 | 28 | private func stressTest(in storage: KeyValueStorageType, timeout: TimeInterval = 100) { 29 | let group = DispatchGroup() 30 | let keyNames = (0...1000).map { _ in UUID().uuidString } 31 | let keys = keyNames.map { KeyValueStorageKey(name: $0, storage: storage) } 32 | let promise = expectation(description: "wait for threads") 33 | 34 | for key in keys { 35 | group.enter() 36 | 37 | DispatchQueue.global().async { 38 | switch (0...90).randomElement()! { 39 | case 0...20: 40 | _ = self.storage.fetch(forKey: key) 41 | case 21...40: 42 | self.storage.set("xxx", forKey: key) 43 | case 41...60: 44 | self.storage.delete(forKey: key) 45 | case 61...80: 46 | self.storage.set("xxx", forKey: key) 47 | default: 48 | self.storage.clear() 49 | } 50 | 51 | group.leave() 52 | } 53 | } 54 | 55 | group.notify(queue: .main) { 56 | promise.fulfill() 57 | } 58 | 59 | wait(for: [promise], timeout: timeout) 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Storages/InMemoryStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InMemoryStorage.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 11.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Data Storage 11 | 12 | @InMemoryActor 13 | open class InMemoryStorage: KeyValueDataStorage, @unchecked Sendable { 14 | 15 | // MARK: Properties 16 | 17 | internal static var container = [Domain?: [Key: Data]]() // not thread safe, for internal use only 18 | public let domain: Domain? 19 | 20 | // MARK: Initializers 21 | 22 | public required init() { 23 | self.domain = nil 24 | } 25 | 26 | public required init(domain: Domain) { 27 | self.domain = domain 28 | } 29 | 30 | // MARK: Main Functionality 31 | 32 | public func fetch(forKey key: Key) -> Data? { 33 | Self.container[domain]?[key] 34 | } 35 | 36 | public func save(_ value: Data, forKey key: Key) { 37 | if Self.container[domain] == nil { 38 | Self.container[domain] = [:] 39 | } 40 | 41 | Self.container[domain]?[key] = value 42 | } 43 | 44 | public func set(_ value: Data?, forKey key: Key) { 45 | if let value = value { 46 | save(value, forKey: key) 47 | } else { 48 | delete(forKey: key) 49 | } 50 | } 51 | 52 | public func delete(forKey key: Key) { 53 | Self.container[domain]?[key] = nil 54 | } 55 | 56 | public func clear() { 57 | Self.container[domain] = [:] 58 | } 59 | } 60 | 61 | // MARK: - Associated Types 62 | 63 | public extension InMemoryStorage { 64 | typealias Key = String 65 | typealias Domain = String 66 | 67 | enum Error: KeyValueDataStorageError { 68 | case unknown 69 | } 70 | } 71 | 72 | // MARK: - Global Actors 73 | 74 | @globalActor 75 | public final class InMemoryActor { 76 | public actor Actor { } 77 | public static let shared = Actor() 78 | } 79 | -------------------------------------------------------------------------------- /Sources/KeyValueStorageLegacyWrapper/KeyValueStoragePropertyWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueStoragePropertyWrapper.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 09.12.23. 6 | // 7 | 8 | import Combine 9 | import KeyValueStorageLegacy 10 | 11 | fileprivate final class KeyValueStoragePreferences { 12 | static let shared = KeyValueStoragePreferences() 13 | 14 | var publishers = [AnyHashable: Any]() 15 | } 16 | 17 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 18 | @propertyWrapper 19 | public struct Storage { 20 | typealias Key = KeyValueStorageKey 21 | 22 | private let preferences = KeyValueStoragePreferences.shared 23 | private let publisherKey: KeyValueStoragePublisherKey 24 | private let storage: KeyValueStorage 25 | private let key: Key 26 | 27 | private var _publisher: PassthroughSubject { 28 | (preferences.publishers[publisherKey] as! PassthroughSubject) 29 | } 30 | 31 | public var wrappedValue: Value? { 32 | get { 33 | storage.fetch(forKey: key) 34 | } 35 | 36 | nonmutating set { 37 | storage.set(newValue, forKey: key) 38 | _publisher.send(newValue) 39 | } 40 | } 41 | 42 | public var publisher: AnyPublisher { 43 | _publisher.eraseToAnyPublisher() 44 | } 45 | 46 | public init(key: KeyValueStorageKey, storage: KeyValueStorage = .default) { 47 | self.key = key 48 | self.storage = storage 49 | self.publisherKey = .init(serviceName: storage.serviceName, key: key) 50 | 51 | if preferences.publishers[publisherKey] == nil { 52 | preferences.publishers[publisherKey] = PassthroughSubject() 53 | } 54 | } 55 | 56 | private struct KeyValueStoragePublisherKey: Hashable { 57 | let serviceName: String 58 | let key: Key 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "KeyValueStorage", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | .watchOS(.v6), 12 | .tvOS(.v13) 13 | ], 14 | products: [ 15 | .library( 16 | name: "KeyValueStorage", 17 | targets: ["KeyValueStorage"]), 18 | 19 | // .library( 20 | // name: "KeyValueStorageLegacy", 21 | // targets: ["KeyValueStorageLegacy"]), 22 | // .library( 23 | // name: "KeyValueStorageLegacyWrapper", 24 | // targets: ["KeyValueStorageLegacyWrapper"]), 25 | // .library( 26 | // name: "KeyValueStorageLegacySwiftUI", 27 | // targets: ["KeyValueStorageLegacySwiftUI"]), 28 | ], 29 | targets: [ 30 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 31 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 32 | .target( 33 | name: "KeyValueStorage", 34 | dependencies: [], 35 | resources: [.process("Resources/PrivacyInfo.xcprivacy")], 36 | swiftSettings: [ 37 | .enableExperimentalFeature("StrictConcurrency"), 38 | ]), 39 | .testTarget( 40 | name: "KeyValueStorageTests", 41 | dependencies: ["KeyValueStorage"]), 42 | 43 | // .target( 44 | // name: "KeyValueStorageLegacy", 45 | // dependencies: []), 46 | // .target( 47 | // name: "KeyValueStorageLegacyWrapper", 48 | // dependencies: [.target(name: "KeyValueStorageLegacy")]), 49 | // .target( 50 | // name: "KeyValueStorageLegacySwiftUI", 51 | // dependencies: [.target(name: "KeyValueStorageLegacyWrapper")]), 52 | // .testTarget( 53 | // name: "KeyValueStorageLegacyTests", 54 | // dependencies: ["KeyValueStorageLegacy", "KeyValueStorageLegacyWrapper", "KeyValueStorageLegacySwiftUI"]), 55 | ] 56 | ) 57 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Storages/UserDefaultsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsStorage.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 11.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Data Storage 11 | 12 | @UserDefaultsActor 13 | open class UserDefaultsStorage: KeyValueDataStorage, @unchecked Sendable { 14 | 15 | // MARK: Properties 16 | 17 | private let userDefaults: UserDefaults 18 | public let domain: Domain? 19 | 20 | // MARK: Initializers 21 | 22 | public required init() { 23 | self.domain = nil 24 | self.userDefaults = .standard 25 | } 26 | 27 | public required init(domain: Domain) throws { 28 | guard let defaults = UserDefaults(suiteName: domain) else { 29 | throw Error.failedToInitSharedDefaults 30 | } 31 | 32 | self.domain = domain 33 | self.userDefaults = defaults 34 | } 35 | 36 | public init(userDefaults: UserDefaults) { 37 | self.userDefaults = userDefaults 38 | self.domain = nil 39 | } 40 | 41 | // MARK: Main Functionality 42 | 43 | public func fetch(forKey key: Key) -> Data? { 44 | userDefaults.data(forKey: key) 45 | } 46 | 47 | public func save(_ value: Data, forKey key: Key) { 48 | userDefaults.set(value, forKey: key) 49 | } 50 | 51 | public func delete(forKey key: Key) { 52 | userDefaults.removeObject(forKey: key) 53 | } 54 | 55 | public func set(_ value: Data?, forKey key: Key) { 56 | if let value = value { 57 | save(value, forKey: key) 58 | } else { 59 | delete(forKey: key) 60 | } 61 | } 62 | 63 | public func clear() { 64 | userDefaults.removePersistentDomain(forName: domain ?? Self.defaultGroup) 65 | } 66 | } 67 | 68 | // MARK: - Associated Types 69 | 70 | public extension UserDefaultsStorage { 71 | typealias Key = String 72 | typealias Domain = String 73 | 74 | enum Error: KeyValueDataStorageError { 75 | case failedToInitSharedDefaults 76 | } 77 | } 78 | 79 | // MARK: - Global Actors 80 | 81 | @globalActor 82 | public final class UserDefaultsActor { 83 | public actor Actor { } 84 | public static let shared = Actor() 85 | } 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## OS Generated 6 | .DS_Store 7 | ../.DS_Store 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # 43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | # Package.resolved 47 | # *.xcodeproj 48 | # 49 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 50 | # hence it is not needed unless you have added a package configuration file to your project 51 | .swiftpm 52 | 53 | .build/ 54 | 55 | # CocoaPods 56 | # 57 | # We recommend against adding the Pods directory to your .gitignore. However 58 | # you should judge for yourself, the pros and cons are mentioned at: 59 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 60 | # 61 | # Pods/ 62 | # 63 | # Add this line if you want to avoid checking in source code from the Xcode workspace 64 | # *.xcworkspace 65 | 66 | # Carthage 67 | # 68 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 69 | # Carthage/Checkouts 70 | 71 | Carthage/Build/ 72 | 73 | # Accio dependency management 74 | Dependencies/ 75 | .accio/ 76 | 77 | # fastlane 78 | # 79 | # It is recommended to not store the screenshots in the git repo. 80 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 81 | # For more information about the recommended setup visit: 82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 83 | 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots/**/*.png 87 | fastlane/test_output 88 | 89 | # Code Injection 90 | # 91 | # After new code Injection tools there's a generated folder /iOSInjectionProject 92 | # https://github.com/johnno1962/injectionforxcode 93 | 94 | iOSInjectionProject/ 95 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageLegacyTests/KeychainHelperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainHelperTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/28/22. 6 | // 7 | 8 | import XCTest 9 | @testable import KeyValueStorageLegacy 10 | 11 | #if os(macOS) 12 | class KeychainHelperTests: XCTestCase { 13 | private var helper: KeychainHelper! 14 | 15 | override func setUp() { 16 | helper = KeychainHelper(serviceName: "asd") 17 | } 18 | 19 | override func tearDown() { 20 | helper.removeAll() 21 | } 22 | 23 | func testMain() throws { 24 | // Given 25 | let data = "secret".data(using: .utf8)! 26 | let newData = "new".data(using: .utf8)! 27 | let key = "key" 28 | 29 | // When - Then 30 | XCTAssertNil(helper.get(forKey: key)) 31 | XCTAssertNil(helper.get(forKey: key, withAccessibility: .whenUnlocked)) 32 | XCTAssertNil(helper.get(forKey: key, isSynchronizable: true)) 33 | 34 | // When 35 | XCTAssertTrue(helper.set(data, forKey: key)) 36 | 37 | // Then 38 | XCTAssertEqual(helper.get(forKey: key), data) 39 | XCTAssertEqual(helper.get(forKey: key, withAccessibility: .whenUnlocked), data) 40 | XCTAssertNil(helper.get(forKey: key, isSynchronizable: true)) 41 | 42 | // When 43 | XCTAssertTrue(helper.set(newData, forKey: key)) 44 | 45 | // Then 46 | XCTAssertEqual(helper.get(forKey: key), newData) 47 | XCTAssertEqual(helper.get(forKey: key, withAccessibility: .whenUnlocked), newData) 48 | XCTAssertNil(helper.get(forKey: key, isSynchronizable: true)) 49 | 50 | // When 51 | XCTAssertTrue(helper.remove(forKey: key)) 52 | 53 | // Then 54 | XCTAssertNil(helper.get(forKey: key)) 55 | XCTAssertNil(helper.get(forKey: key, withAccessibility: .whenUnlocked)) 56 | XCTAssertNil(helper.get(forKey: key, isSynchronizable: true)) 57 | 58 | // When 59 | XCTAssertFalse(helper.set(data, forKey: key, isSynchronizable: true)) 60 | 61 | // Then 62 | XCTAssertNil(helper.get(forKey: key)) 63 | XCTAssertNil(helper.get(forKey: key, withAccessibility: .whenUnlocked)) 64 | XCTAssertNil(helper.get(forKey: key, withAccessibility: .afterFirstUnlock)) 65 | XCTAssertNil(helper.get(forKey: key, isSynchronizable: true)) 66 | } 67 | 68 | func testAccessibilityRawValues() { 69 | // Given 70 | XCTAssertEqual(KeychainAccessibility.afterFirstUnlock.key, String(kSecAttrAccessibleAfterFirstUnlock)) 71 | XCTAssertEqual(KeychainAccessibility.afterFirstUnlockThisDeviceOnly.key, String(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)) 72 | XCTAssertEqual(KeychainAccessibility.whenPasscodeSetThisDeviceOnly.key, String(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly)) 73 | XCTAssertEqual(KeychainAccessibility.whenUnlocked.key, String(kSecAttrAccessibleWhenUnlocked)) 74 | XCTAssertEqual(KeychainAccessibility.whenUnlockedThisDeviceOnly.key, String(kSecAttrAccessibleWhenUnlockedThisDeviceOnly)) 75 | } 76 | 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Storages/Layers/KeyValueCodingStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueCodingStorage.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 11.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Coding Storage Key 11 | 12 | public struct KeyValueCodingStorageKey: Sendable { 13 | public let key: Storage.Key 14 | public let codingType: Value.Type 15 | 16 | public init(key: Storage.Key) { 17 | self.key = key 18 | self.codingType = Value.self 19 | } 20 | 21 | internal init(key: Storage.Key, codingType: Value.Type) { 22 | self.key = key 23 | self.codingType = codingType 24 | } 25 | } 26 | 27 | extension KeyValueCodingStorageKey: Hashable { 28 | public static func == (lhs: Self, rhs: Self) -> Bool { 29 | lhs.key == rhs.key && 30 | lhs.codingType == rhs.codingType 31 | } 32 | 33 | public func hash(into hasher: inout Hasher) { 34 | hasher.combine(key) 35 | hasher.combine(String(describing: codingType)) 36 | } 37 | } 38 | 39 | // MARK: - Coding Storage 40 | 41 | @CodingStorageActor 42 | open class KeyValueCodingStorage: @unchecked Sendable, Clearing { 43 | 44 | // MARK: Properties 45 | 46 | private let coder: DataCoder 47 | private let storage: Storage 48 | 49 | public var domain: Storage.Domain? { 50 | storage.domain 51 | } 52 | 53 | // MARK: Initializers 54 | 55 | public init(storage: Storage, coder: DataCoder = JSONDataCoder()) { 56 | self.coder = coder 57 | self.storage = storage 58 | } 59 | 60 | // MARK: Main Functionality 61 | 62 | public func fetch(forKey key: KeyValueCodingStorageKey) async throws -> Value? { 63 | if let data = try await storage.fetch(forKey: key.key) { 64 | return try await coder.decode(data) 65 | } 66 | 67 | return nil 68 | } 69 | 70 | public func save(_ value: Value, forKey key: KeyValueCodingStorageKey) async throws { 71 | let encoded = try await coder.encode(value) 72 | try await storage.save(encoded, forKey: key.key) 73 | } 74 | 75 | public func set(_ value: Value?, forKey key: KeyValueCodingStorageKey) async throws { 76 | if let value { 77 | try await save(value, forKey: key) 78 | } else { 79 | try await delete(forKey: key) 80 | } 81 | } 82 | 83 | public func delete(forKey key: KeyValueCodingStorageKey) async throws { 84 | try await storage.delete(forKey: key.key) 85 | } 86 | 87 | public func clear() async throws { 88 | try await storage.clear() 89 | } 90 | } 91 | 92 | // MARK: - Helper Protocols 93 | 94 | protocol Clearing: Sendable { 95 | func clear() async throws 96 | } 97 | 98 | // MARK: - Global Actors 99 | 100 | @globalActor 101 | public final class CodingStorageActor { 102 | public actor Actor { } 103 | public static let shared = Actor() 104 | } 105 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Storages/Layers/KeyValueObservableStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueObservableStorage.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 30.12.23. 6 | // 7 | 8 | import Combine 9 | 10 | @CodingStorageActor 11 | private final class KeyValueObservations { 12 | fileprivate static var observations = [AnyHashable?: [AnyHashable: Any]]() 13 | } 14 | 15 | @CodingStorageActor 16 | open class KeyValueObservableStorage: KeyValueCodingStorage, @unchecked Sendable { 17 | 18 | // MARK: Observations 19 | 20 | public func stream(forKey key: KeyValueCodingStorageKey) -> AsyncStream { 21 | return AsyncStream(bufferingPolicy: .unbounded) { continuation in 22 | let publisher: AnyPublisher = publisher(forKey: key) 23 | let subscription = publisher.sink { 24 | continuation.yield($0) 25 | } 26 | 27 | continuation.onTermination = { _ in 28 | subscription.cancel() 29 | } 30 | } 31 | } 32 | 33 | public func publisher(forKey key: KeyValueCodingStorageKey) -> AnyPublisher { 34 | let mapPublisher = { (publisher: PassthroughSubject) -> AnyPublisher in 35 | publisher 36 | .map { 37 | if let value = $0.value { 38 | return value as? Value 39 | } 40 | 41 | return nil 42 | } 43 | .eraseToAnyPublisher() 44 | } 45 | 46 | if let observation = KeyValueObservations.observations[domain]?[key], 47 | let publisher = observation as? PassthroughSubject { 48 | return mapPublisher(publisher) 49 | } 50 | 51 | if KeyValueObservations.observations[domain] == nil { 52 | KeyValueObservations.observations[domain] = [:] 53 | } 54 | 55 | let publisher = PassthroughSubject() 56 | KeyValueObservations.observations[domain]?[key] = publisher 57 | return mapPublisher(publisher) 58 | } 59 | 60 | /* AsyncPublisher emits most of the value changes*/ 61 | // @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) 62 | // public func asyncPublisher(forKey key: KeyValueCodingStorageKey) 63 | // async -> AsyncPublisher> { 64 | // AsyncPublisher(publisher(forKey: key)) 65 | // } 66 | 67 | // MARK: Main Functionality 68 | 69 | public override func save(_ value: Value, forKey key: KeyValueCodingStorageKey) async throws { 70 | try await super.save(value, forKey: key) 71 | publisher(for: key)?.send(.init(value: value)) 72 | } 73 | 74 | public override func delete(forKey key: KeyValueCodingStorageKey) async throws { 75 | try await super.delete(forKey: key) 76 | publisher(for: key)?.send(.init()) 77 | } 78 | 79 | public override func clear() async throws { 80 | try await super.clear() 81 | 82 | for observation in (KeyValueObservations.observations[domain] ?? [:]).values { 83 | if let publisher = observation as? PassthroughSubject { 84 | publisher.send(.init()) 85 | } 86 | } 87 | } 88 | 89 | // MARK: Helpers 90 | 91 | private func publisher(for key: AnyHashable) -> PassthroughSubject? { 92 | KeyValueObservations.observations[domain]?[key] as? PassthroughSubject 93 | } 94 | 95 | // MARK: Inner Hidden Types 96 | 97 | private struct Container { 98 | var value: Any? 99 | } 100 | } 101 | 102 | extension AnyCancellable: @unchecked Sendable { } 103 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Storages/KeychainStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainStorage.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 11.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Data Storage 11 | 12 | @KeychainActor 13 | open class KeychainStorage: KeyValueDataStorage, @unchecked Sendable { 14 | 15 | // MARK: Properties 16 | 17 | private let keychain: KeychainWrapper 18 | public let domain: Domain? 19 | 20 | // MARK: Initializers 21 | 22 | public required init() { 23 | self.domain = nil 24 | self.keychain = KeychainWrapper(serviceName: Self.defaultGroup) 25 | } 26 | 27 | public required init(domain: Domain) { 28 | self.domain = domain 29 | self.keychain = KeychainWrapper(serviceName: Self.defaultGroup, accessGroup: domain.accessGroup) 30 | } 31 | 32 | public init(keychain: KeychainWrapper) { 33 | self.keychain = keychain 34 | self.domain = nil 35 | } 36 | 37 | // MARK: Main Functionality 38 | 39 | public func fetch(forKey key: Key) throws -> Data? { 40 | try execute { 41 | try keychain.get(forKey: key.name, withAccessibility: key.accessibility, isSynchronizable: key.isSynchronizable) 42 | } 43 | } 44 | 45 | public func save(_ value: Data, forKey key: Key) throws { 46 | try execute { 47 | try keychain.set(value, forKey: key.name, withAccessibility: key.accessibility, isSynchronizable: key.isSynchronizable) 48 | } 49 | } 50 | 51 | public func delete(forKey key: Key) throws { 52 | try execute { 53 | try keychain.remove(forKey: key.name, withAccessibility: key.accessibility, isSynchronizable: key.isSynchronizable) 54 | } 55 | } 56 | 57 | public func set(_ value: Data?, forKey key: Key) throws { 58 | if let value = value { 59 | try save(value, forKey: key) 60 | } else { 61 | try delete(forKey: key) 62 | } 63 | } 64 | 65 | public func clear() throws { 66 | try execute { 67 | try keychain.removeAll() 68 | } 69 | } 70 | 71 | // MARK: Helpers 72 | 73 | private func convert(error: Swift.Error) -> Error { 74 | if case let .status(status) = error as? KeychainWrapperError { 75 | return .os(status) 76 | } 77 | 78 | return .other(error) 79 | } 80 | 81 | @discardableResult 82 | private func execute(_ block: () throws -> T) rethrows -> T { 83 | do { 84 | return try block() 85 | } catch { 86 | throw convert(error: error) 87 | } 88 | } 89 | } 90 | 91 | // MARK: - Associated Types 92 | 93 | public extension KeychainStorage { 94 | struct Key: KeyValueDataStorageKey { 95 | public let name: String 96 | public let accessibility: KeychainAccessibility? 97 | public let isSynchronizable: Bool 98 | 99 | public init(name: String, accessibility: KeychainAccessibility? = nil, isSynchronizable: Bool = false) { 100 | self.name = name 101 | self.accessibility = accessibility 102 | self.isSynchronizable = isSynchronizable 103 | } 104 | } 105 | 106 | struct Domain: KeyValueDataStorageDomain { 107 | public let groupId: String 108 | public let teamId: String 109 | 110 | public init(groupId: String, teamId: String) { 111 | self.groupId = groupId 112 | self.teamId = teamId 113 | } 114 | 115 | public var accessGroup: String { 116 | teamId + "." + groupId 117 | } 118 | } 119 | 120 | enum Error: KeyValueDataStorageError { 121 | case os(OSStatus) 122 | case other(Swift.Error) 123 | } 124 | } 125 | 126 | // MARK: - Global Actors 127 | 128 | @globalActor 129 | public final class KeychainActor { 130 | public actor Actor { } 131 | public static let shared = Actor() 132 | } 133 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageLegacyTests/KeyValueStoragePropertyWrapperTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueStoragePropertyWrapperTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 10.12.23. 6 | // 7 | 8 | import XCTest 9 | import SwiftUI 10 | @testable import KeyValueStorageLegacy 11 | @testable import KeyValueStorageLegacyWrapper 12 | @testable import KeyValueStorageLegacySwiftUI 13 | 14 | #if os(macOS) 15 | @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) 16 | final class KeyValueStoragePropertyWrapperTests: XCTestCase { 17 | private var storage: KeyValueStorage! 18 | 19 | override func setUp() { 20 | storage = KeyValueStorage() 21 | } 22 | 23 | override func tearDown() { 24 | storage.clear() 25 | } 26 | 27 | func testWrapper() { 28 | // Given 29 | let key = KeyValueStorageKey(name: "key", storage: .inMemory) 30 | 31 | // When 32 | @Storage(key: key) var int: Int? 33 | 34 | // Then 35 | XCTAssertNil(int) 36 | XCTAssertNil(storage.fetch(forKey: key)) 37 | 38 | // When 39 | int = 10 40 | 41 | // Then 42 | XCTAssertEqual(int, 10) 43 | XCTAssertEqual(storage.fetch(forKey: key), 10) 44 | 45 | // When 46 | storage.save(13, forKey: key) 47 | 48 | // Then 49 | XCTAssertEqual(int, 13) 50 | XCTAssertEqual(storage.fetch(forKey: key), 13) 51 | 52 | // When 53 | storage.delete(forKey: key) 54 | 55 | // Then 56 | XCTAssertNil(int) 57 | XCTAssertNil(storage.fetch(forKey: key)) 58 | 59 | // When 60 | storage.save(18, forKey: key) 61 | 62 | // Then 63 | XCTAssertEqual(int, 18) 64 | XCTAssertEqual(storage.fetch(forKey: key), 18) 65 | 66 | // When 67 | int = nil 68 | // Then 69 | XCTAssertNil(int) 70 | XCTAssertNil(storage.fetch(forKey: key)) 71 | } 72 | 73 | func testPublishers() { 74 | // Given 75 | let key1 = KeyValueStorageKey(name: "key", storage: .inMemory) 76 | let key2 = KeyValueStorageKey(name: "key", storage: .inMemory) 77 | 78 | var sink1Called = false 79 | var sink2Called = false 80 | var sink3Called = false 81 | 82 | @Storage(key: key1) var int1: Int? 83 | @Storage(key: key2) var int2: Int? 84 | 85 | let subscription1 = _int1.publisher.sink { value in 86 | // Then 87 | XCTAssertEqual(int1, int2) 88 | XCTAssertEqual(int1, value) 89 | sink1Called = true 90 | } 91 | 92 | let subscription2 = _int2.publisher.sink { value in 93 | // Then 94 | XCTAssertEqual(int1, int2) 95 | XCTAssertEqual(value, int2) 96 | sink2Called = true 97 | } 98 | 99 | let subscription3 = _int2.publisher.sink { value in 100 | // Then 101 | XCTAssertEqual(int1, int2) 102 | XCTAssertEqual(value, int2) 103 | sink3Called = true 104 | } 105 | 106 | // When 107 | int1 = 10 108 | int2 = 20 109 | 110 | // Then 111 | XCTAssertEqual(int1, int2) 112 | XCTAssertTrue(sink1Called) 113 | XCTAssertTrue(sink2Called) 114 | XCTAssertTrue(sink3Called) 115 | XCTAssertNotNil(subscription1) 116 | XCTAssertNotNil(subscription2) 117 | XCTAssertNotNil(subscription3) 118 | } 119 | 120 | func testBinding() { 121 | // Given 122 | let key = KeyValueStorageKey(name: "key", storage: .inMemory) 123 | @ObservedStorage(key: key) var int: Int? 124 | int = 10 125 | 126 | // When 127 | @Binding var bindedInt: Int? 128 | _bindedInt = $int 129 | 130 | // Then 131 | XCTAssertEqual(bindedInt, 10) 132 | 133 | // When 134 | bindedInt = 77 135 | // Then 136 | XCTAssertEqual(int, 77) 137 | } 138 | } 139 | 140 | #endif 141 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Storages/FileStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileStorage.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 17.12.23. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Data Storage 11 | 12 | @FileActor 13 | open class FileStorage: KeyValueDataStorage, @unchecked Sendable { 14 | 15 | // MARK: Properties 16 | 17 | private let fileManager: FileManager 18 | private let root: URL 19 | public let domain: Domain? 20 | 21 | // MARK: Initializers 22 | 23 | public required init() throws { 24 | let fileManager = FileManager.default 25 | guard let url = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { 26 | throw Error.failedToFindDocumentsDirectory 27 | } 28 | 29 | self.root = url.appendingPathComponent(Self.defaultGroup, isDirectory: true) 30 | self.domain = nil 31 | self.fileManager = fileManager 32 | } 33 | 34 | public required init(domain: Domain) throws { 35 | let fileManager = FileManager.default 36 | guard let url = fileManager.containerURL(forSecurityApplicationGroupIdentifier: domain) else { 37 | throw Error.failedToInitSharedDirectory 38 | } 39 | 40 | self.root = url.appendingPathComponent(Self.defaultGroup, isDirectory: true) 41 | self.domain = domain 42 | self.fileManager = fileManager 43 | } 44 | 45 | public init(fileManager: FileManager, root: URL) { 46 | self.fileManager = fileManager 47 | self.root = root 48 | self.domain = nil 49 | } 50 | 51 | // MARK: Main Functionality 52 | 53 | public func fetch(forKey key: Key) throws -> Data? { 54 | fileManager.contents(atPath: directory(for: key).path) 55 | } 56 | 57 | public func save(_ value: Data, forKey key: Key) throws { 58 | try execute { 59 | let directory = directory(for: key) 60 | let directoryPath = directory.path 61 | 62 | try createDirectoryIfDoesntExist(path: root.path) 63 | try deleteFileIfExists(path: directoryPath) 64 | 65 | if !fileManager.createFile(atPath: directoryPath, contents: value) { 66 | throw Error.failedToSave 67 | } 68 | } 69 | } 70 | 71 | public func delete(forKey key: Key) throws { 72 | try execute { 73 | try deleteFileIfExists(path: directory(for: key).path) 74 | } 75 | } 76 | 77 | public func set(_ value: Data?, forKey key: Key) throws { 78 | if let value = value { 79 | try save(value, forKey: key) 80 | } else { 81 | try delete(forKey: key) 82 | } 83 | } 84 | 85 | public func clear() throws { 86 | try execute { 87 | if let fileNames = try? fileManager.contentsOfDirectory(atPath: root.path) { 88 | for fileName in fileNames { 89 | let path = root.appendingPathComponent(fileName).path 90 | try deleteFileIfExists(path: path) 91 | } 92 | } 93 | } 94 | } 95 | 96 | // MARK: Helpers 97 | 98 | private func deleteFileIfExists(path: String) throws { 99 | do { 100 | try fileManager.removeItem(atPath: path) 101 | } catch CocoaError.fileNoSuchFile { 102 | // ok 103 | } catch { 104 | throw error 105 | } 106 | } 107 | 108 | private func createDirectoryIfDoesntExist(path: String) throws { 109 | do { 110 | try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true) 111 | } catch CocoaError.fileWriteFileExists { 112 | // ok 113 | } catch { 114 | throw error 115 | } 116 | } 117 | 118 | private func directory(for key: Key) -> URL { 119 | root.appendingPathComponent(key) 120 | } 121 | 122 | private func convert(error: Swift.Error) -> Error { 123 | if let error = error as? FileStorage.Error { 124 | return error 125 | } 126 | 127 | return .other(error) 128 | } 129 | 130 | @discardableResult 131 | private func execute(_ block: () throws -> T) rethrows -> T { 132 | do { 133 | return try block() 134 | } catch { 135 | throw convert(error: error) 136 | } 137 | } 138 | } 139 | 140 | // MARK: - Associated Types 141 | 142 | public extension FileStorage { 143 | typealias Key = String 144 | typealias Domain = String 145 | 146 | enum Error: KeyValueDataStorageError { 147 | case failedToSave 148 | case failedToInitSharedDirectory 149 | case failedToFindDocumentsDirectory 150 | case other(Swift.Error) 151 | } 152 | } 153 | 154 | // MARK: - Global Actors 155 | 156 | @globalActor 157 | public final class FileActor { 158 | public actor Actor { } 159 | public static let shared = Actor() 160 | } 161 | -------------------------------------------------------------------------------- /Sources/KeyValueStorageLegacy/KeyValueStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueStorage.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/27/22. 6 | // 7 | 8 | import Foundation 9 | 10 | /// The main class responsible for manipulating the storage. 11 | open class KeyValueStorage { 12 | private let encoder = JSONEncoder() 13 | private let decoder = JSONDecoder() 14 | private let userDefaults: UserDefaults 15 | private let keychain: KeychainHelper 16 | private let serialQueue = DispatchQueue(label: "KeyValueStorage.default.queue", qos: .userInitiated) 17 | private static let defaultServiceName = Bundle.main.bundleIdentifier.unwrapped("KeyValueStorage") 18 | private static var inMemoryStorage = [String: [String: Any]]() 19 | public var serviceName: String { accessGroup.unwrapped(Self.defaultServiceName) } 20 | 21 | /// `accessGroup` is used to identify which Access Group all items belongs to. This allows using shared access between different applications. 22 | public let accessGroup: String? 23 | 24 | /// Default initializer, which doesn't allow using shared access between different applications. 25 | public init() { 26 | self.userDefaults = UserDefaults.standard 27 | self.keychain = KeychainHelper(serviceName: Self.defaultServiceName) 28 | self.accessGroup = nil 29 | } 30 | 31 | /// This initializer allows using shared access between different applications if appropriately configured. 32 | /// 33 | /// - parameter accessGroup: The access group name. Make sure to add appropriate capabilities in your app and register the name before using it. 34 | /// - parameter teamID: The Team ID of your development team. It can be found on developer.apple.com. 35 | public init(accessGroup: String, teamID: String) { 36 | self.accessGroup = accessGroup 37 | self.userDefaults = UserDefaults(suiteName: accessGroup)! 38 | self.keychain = KeychainHelper(serviceName: Self.defaultServiceName, accessGroup: teamID + "." + accessGroup) 39 | } 40 | 41 | /// Saves the item and associates it with the key or overrides the value if there is already such item. 42 | /// 43 | /// - parameter value: The item to be saved. 44 | /// - parameter key: The key to uniquely identify the item. 45 | open func save(_ value: T, forKey key: KeyValueStorageKey) { 46 | serialQueue.sync { 47 | switch key.storageType { 48 | case .inMemory: 49 | var data = Self.inMemoryStorage[serviceName].unwrapped([:]) 50 | data[key.name] = value 51 | Self.inMemoryStorage[serviceName] = data 52 | case .userDefaults: 53 | guard let data = try? self.encoder.encode([key.name: value]) else { return } 54 | self.userDefaults.set(data, forKey: key.name) 55 | case let .keychain(accessibility, synchronizable): 56 | guard let data = try? self.encoder.encode([key.name: value]) else { return } 57 | self.keychain.set(data, forKey: key.name, withAccessibility: accessibility, isSynchronizable: synchronizable) 58 | } 59 | } 60 | } 61 | 62 | /// Fetches the item associated with the key. 63 | /// 64 | /// - parameter key: The key to uniquely identify the item. 65 | /// - returns: The item or nil if there is no item associated with the specified key. 66 | open func fetch(forKey key: KeyValueStorageKey) -> T? { 67 | serialQueue.sync { 68 | var fetchedData: Data? 69 | 70 | switch key.storageType { 71 | case .inMemory: 72 | return Self.inMemoryStorage[serviceName]?[key.name] as? T 73 | case .userDefaults: 74 | fetchedData = userDefaults.data(forKey: key.name) 75 | case let .keychain(accessibility, synchronizable): 76 | fetchedData = keychain.get(forKey: key.name, withAccessibility: accessibility, isSynchronizable: synchronizable) 77 | } 78 | 79 | guard let data = fetchedData else { return nil } 80 | return (try? decoder.decode([String: T].self, from: data))?[key.name] 81 | } 82 | } 83 | 84 | /// Deletes the item associated with the key or does nothing if there is no such item. 85 | /// 86 | /// - parameter value: The item to be saved. 87 | /// - parameter key: The key to uniquely identify the item. 88 | open func delete(forKey key: KeyValueStorageKey) { 89 | serialQueue.sync { 90 | switch key.storageType { 91 | case .inMemory: 92 | Self.inMemoryStorage[serviceName]?[key.name] = nil 93 | case .userDefaults: 94 | self.userDefaults.removeObject(forKey: key.name) 95 | case let .keychain(accessibility, synchronizable): 96 | self.keychain.remove(forKey: key.name, withAccessibility: accessibility, isSynchronizable: synchronizable) 97 | } 98 | } 99 | } 100 | 101 | /// Sets the item identified by the key to the provided value. 102 | /// 103 | /// - parameter value: The item to be saved or deleted if nil is provided. 104 | /// - parameter forKey: The key to uniquely identify the item. 105 | open func set(_ value: T?, forKey key: KeyValueStorageKey) { 106 | if let value = value { 107 | save(value, forKey: key) 108 | } else { 109 | delete(forKey: key) 110 | } 111 | } 112 | 113 | /// Clears all the items in all storage types. 114 | open func clear() { 115 | serialQueue.sync { 116 | Self.inMemoryStorage[self.serviceName] = nil 117 | self.userDefaults.removePersistentDomain(forName: self.accessGroup ?? Self.defaultServiceName) 118 | self.keychain.removeAll() 119 | } 120 | } 121 | } 122 | 123 | public extension KeyValueStorage { 124 | static let `default` = KeyValueStorage() 125 | } 126 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/Helpers/KeychainWrapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainWrapper.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/27/22. 6 | // 7 | 8 | import Foundation 9 | import Security 10 | 11 | enum KeychainWrapperError: Error { 12 | case status(OSStatus) 13 | } 14 | 15 | /// A wrapper class which allows to use Keychain it in a similar manner to User Defaults. 16 | open class KeychainWrapper: @unchecked Sendable { 17 | 18 | /// `serviceName` is used to uniquely identify this keychain accessor. 19 | let serviceName: String 20 | 21 | /// `accessGroup` is used to identify which Keychain Access Group this entry belongs to. This allows you to use shared keychain access between different applications. 22 | let accessGroup: String? 23 | 24 | init(serviceName: String, accessGroup: String? = nil) { 25 | self.serviceName = serviceName 26 | self.accessGroup = accessGroup 27 | } 28 | 29 | func get(forKey key: String, 30 | withAccessibility accessibility: KeychainAccessibility? = nil, 31 | isSynchronizable: Bool = false) throws -> Data? { 32 | var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable) 33 | keychainQueryDictionary[Self.matchLimit] = kSecMatchLimitOne 34 | keychainQueryDictionary[Self.returnData] = kCFBooleanTrue 35 | 36 | // Search 37 | var result: AnyObject? 38 | let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result) 39 | 40 | switch status { 41 | case errSecSuccess, errSecItemNotFound: 42 | return result as? Data 43 | default: 44 | throw KeychainWrapperError.status(status) 45 | } 46 | } 47 | 48 | func set(_ value: Data, 49 | forKey key: String, 50 | withAccessibility accessibility: KeychainAccessibility? = nil, 51 | isSynchronizable: Bool = false) throws { 52 | var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable) 53 | keychainQueryDictionary[Self.valueData] = value 54 | 55 | let status = SecItemAdd(keychainQueryDictionary as CFDictionary, nil) 56 | if status == errSecDuplicateItem { 57 | try update(value, query: keychainQueryDictionary) 58 | } else if status != errSecSuccess { 59 | throw KeychainWrapperError.status(status) 60 | } 61 | } 62 | 63 | func remove(forKey key: String, 64 | withAccessibility accessibility: KeychainAccessibility? = nil, 65 | isSynchronizable: Bool = false) throws { 66 | let keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable) 67 | 68 | let status = SecItemDelete(keychainQueryDictionary as CFDictionary) 69 | if status != errSecSuccess && status != errSecItemNotFound { 70 | throw KeychainWrapperError.status(status) 71 | } 72 | } 73 | 74 | func removeAll() throws { 75 | var keychainQueryDictionary: [String: Any] = [Self.class: kSecClassGenericPassword] 76 | keychainQueryDictionary[Self.attrService] = serviceName 77 | keychainQueryDictionary[Self.attrAccessGroup] = accessGroup 78 | 79 | let status = SecItemDelete(keychainQueryDictionary as CFDictionary) 80 | if status != errSecSuccess && status != errSecItemNotFound { 81 | throw KeychainWrapperError.status(status) 82 | } 83 | } 84 | 85 | // MARK: - Helpers 86 | 87 | private func update(_ value: Data, query: [String: Any]) throws { 88 | let updateDictionary = [Self.valueData: value] 89 | 90 | let status = SecItemUpdate(query as CFDictionary, updateDictionary as CFDictionary) 91 | if status != errSecSuccess { 92 | throw KeychainWrapperError.status(status) 93 | } 94 | } 95 | 96 | private func query(forKey key: String, 97 | withAccessibility accessibility: KeychainAccessibility? = nil, 98 | isSynchronizable: Bool = false) -> [String: Any] { 99 | var keychainQueryDictionary: [String: Any] = [Self.class: kSecClassGenericPassword] 100 | keychainQueryDictionary[Self.attrService] = serviceName 101 | keychainQueryDictionary[Self.attrAccessible] = accessibility?.key 102 | keychainQueryDictionary[Self.attrAccessGroup] = accessGroup 103 | keychainQueryDictionary[Self.useDataProtection] = kCFBooleanTrue 104 | keychainQueryDictionary[Self.attrAccount] = key 105 | keychainQueryDictionary[Self.attrSynchronizable] = isSynchronizable ? kCFBooleanTrue : kCFBooleanFalse 106 | return keychainQueryDictionary 107 | } 108 | } 109 | 110 | // MARK: - Keys 111 | 112 | extension KeychainWrapper { 113 | private static let `class` = kSecClass as String 114 | private static let matchLimit = kSecMatchLimit as String 115 | private static let returnData = kSecReturnData as String 116 | private static let valueData = kSecValueData as String 117 | private static let attrAccessible = kSecAttrAccessible as String 118 | private static let attrService = kSecAttrService as String 119 | private static let attrGeneric = kSecAttrGeneric as String 120 | private static let attrAccount = kSecAttrAccount as String 121 | private static let attrAccessGroup = kSecAttrAccessGroup as String 122 | private static let attrSynchronizable = kSecAttrSynchronizable as String 123 | private static let returnAttributes = kSecReturnAttributes as String 124 | private static let useDataProtection = kSecUseDataProtectionKeychain as String 125 | 126 | } 127 | 128 | // MARK: - Accessibility 129 | 130 | public enum KeychainAccessibility: Sendable { 131 | case afterFirstUnlock 132 | case afterFirstUnlockThisDeviceOnly 133 | case whenPasscodeSetThisDeviceOnly 134 | case whenUnlocked 135 | case whenUnlockedThisDeviceOnly 136 | // case always (deprecated) 137 | // case alwaysThisDeviceOnly (deprecated) 138 | 139 | var key: String { 140 | switch self { 141 | case .afterFirstUnlock: return kSecAttrAccessibleAfterFirstUnlock as String 142 | case .afterFirstUnlockThisDeviceOnly: return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String 143 | case .whenPasscodeSetThisDeviceOnly: return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly as String 144 | case .whenUnlocked: return kSecAttrAccessibleWhenUnlocked as String 145 | case .whenUnlockedThisDeviceOnly: return kSecAttrAccessibleWhenUnlockedThisDeviceOnly as String 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/KeyValueStorage/UnifiedStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedStorage.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 11.12.23. 6 | // 7 | 8 | import Combine 9 | 10 | // MARK: - Unified Storage Key 11 | 12 | public struct UnifiedStorageKey: Sendable { 13 | public let key: Storage.Key 14 | public let domain: Storage.Domain? 15 | public let codingType: Value.Type 16 | public let storageType: Storage.Type 17 | 18 | public init(key: Storage.Key, domain: Storage.Domain? = nil) { 19 | self.key = key 20 | self.domain = domain 21 | self.codingType = Value.self 22 | self.storageType = Storage.self 23 | } 24 | } 25 | 26 | extension UnifiedStorageKey: Hashable { 27 | public static func == (lhs: Self, rhs: Self) -> Bool { 28 | lhs.key == rhs.key && 29 | lhs.domain == rhs.domain && 30 | lhs.codingType == rhs.codingType && 31 | lhs.storageType == rhs.storageType 32 | } 33 | 34 | public func hash(into hasher: inout Hasher) { 35 | hasher.combine(key) 36 | hasher.combine(domain) 37 | hasher.combine(String(describing: codingType)) 38 | hasher.combine(String(describing: storageType)) 39 | } 40 | } 41 | 42 | // MARK: - Unified Storage Factory 43 | 44 | public protocol UnifiedStorageFactory: Sendable { 45 | func dataStorage(for domain: Storage.Domain?) async throws -> Storage 46 | func codingStorage(for storage: Storage) async throws -> KeyValueCodingStorage 47 | } 48 | 49 | open class DefaultUnifiedStorageFactory: UnifiedStorageFactory, @unchecked Sendable { 50 | public func dataStorage(for domain: Storage.Domain?) async throws -> Storage { 51 | if let domain { 52 | return try await Storage(domain: domain) 53 | } 54 | 55 | return try await Storage() 56 | } 57 | 58 | public func codingStorage(for storage: Storage) async throws -> KeyValueCodingStorage { 59 | await KeyValueCodingStorage(storage: storage) 60 | } 61 | } 62 | 63 | public final class ObservableUnifiedStorageFactory: DefaultUnifiedStorageFactory { 64 | public override func codingStorage(for storage: Storage) async throws -> KeyValueCodingStorage { 65 | await KeyValueObservableStorage(storage: storage) 66 | } 67 | } 68 | 69 | // MARK: - Unified Storage 70 | 71 | public actor UnifiedStorage { 72 | 73 | // MARK: Type Aliases 74 | 75 | public typealias Key = UnifiedStorageKey 76 | 77 | // MARK: Properties 78 | 79 | private var storages = [AnyHashable?: Any]() 80 | private let factory: UnifiedStorageFactory 81 | 82 | // MARK: Initializers 83 | 84 | public init(factory: UnifiedStorageFactory) { 85 | self.factory = factory 86 | } 87 | 88 | public init() { 89 | self.init(factory: DefaultUnifiedStorageFactory()) 90 | } 91 | 92 | // MARK: Main Functionality 93 | 94 | public func fetch(forKey key: Key) async throws -> Value? { 95 | let storage: KeyValueCodingStorage = try await storage(for: key.domain) 96 | return try await storage.fetch(forKey: .init(key: key.key)) 97 | } 98 | 99 | public func save(_ value: Value, forKey key: Key) async throws { 100 | let storage: KeyValueCodingStorage = try await storage(for: key.domain) 101 | try await storage.save(value, forKey: .init(key: key.key)) 102 | } 103 | 104 | public func set(_ value: Value?, forKey key: Key) async throws { 105 | let storage: KeyValueCodingStorage = try await storage(for: key.domain) 106 | try await storage.set(value, forKey: .init(key: key.key)) 107 | } 108 | 109 | public func delete(forKey key: Key) async throws { 110 | let storage: KeyValueCodingStorage = try await storage(for: key.domain) 111 | try await storage.delete(forKey: .init(key: key.key, codingType: key.codingType)) 112 | } 113 | 114 | public func clear(storage: Storage.Type, forDomain domain: Storage.Domain?) async throws { 115 | let storage: KeyValueCodingStorage = try await self.storage(for: domain) 116 | try await storage.clear() 117 | } 118 | 119 | public func clear(storage: Storage.Type) async throws { 120 | for storage in storages.values { 121 | if let casted = storage as? KeyValueCodingStorage { 122 | try await casted.clear() 123 | } 124 | } 125 | } 126 | 127 | public func clear() async throws { 128 | for storage in storages.values { 129 | if let casted = storage as? Clearing { 130 | try await casted.clear() 131 | } 132 | } 133 | } 134 | 135 | // MARK: - Observation 136 | 137 | public func publisher(forKey key: Key) 138 | async throws -> AnyPublisher? { 139 | let storage: KeyValueCodingStorage = try await storage(for: key.domain) 140 | let codingKey = KeyValueCodingStorageKey(key: key.key) 141 | return await (storage as? KeyValueObservableStorage)?.publisher(forKey: codingKey) 142 | } 143 | 144 | public func stream(forKey key: Key) 145 | async throws -> AsyncStream? { 146 | let storage: KeyValueCodingStorage = try await storage(for: key.domain) 147 | let codingKey = KeyValueCodingStorageKey(key: key.key) 148 | return await (storage as? KeyValueObservableStorage)?.stream(forKey: codingKey) 149 | } 150 | 151 | // MARK: Helpers 152 | 153 | private func storage(for domain: Storage.Domain?) async throws -> KeyValueCodingStorage { 154 | let underlyingKey = UnderlyingStorageKey(domain: domain) 155 | if let storage = storages[underlyingKey], let casted = storage as? KeyValueCodingStorage { 156 | return casted 157 | } 158 | 159 | let dataStorage: Storage = try await factory.dataStorage(for: domain) 160 | let codingStorage = try await factory.codingStorage(for: dataStorage) 161 | storages[underlyingKey] = codingStorage 162 | 163 | 164 | return codingStorage 165 | } 166 | } 167 | 168 | extension UnifiedStorage { 169 | private struct UnderlyingStorageKey: Hashable, Sendable { 170 | let domain: Storage.Domain? 171 | let storageType: Storage.Type 172 | 173 | init(domain: Storage.Domain?) { 174 | self.domain = domain 175 | self.storageType = Storage.self 176 | } 177 | 178 | static func == (lhs: Self, rhs: Self) -> Bool { 179 | lhs.domain == rhs.domain && 180 | lhs.storageType == rhs.storageType 181 | } 182 | 183 | func hash(into hasher: inout Hasher) { 184 | hasher.combine(domain) 185 | hasher.combine(String(describing: storageType)) 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Sources/KeyValueStorageLegacy/Helpers/KeychainHelper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeychainHelper.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/27/22. 6 | // 7 | 8 | import Foundation 9 | import Security 10 | 11 | /// A wrapper class which allows to use Keychain it in a similar manner to User Defaults. 12 | final class KeychainHelper { 13 | 14 | /// `serviceName` is used to uniquely identify this keychain accessor. 15 | private(set) var serviceName: String 16 | 17 | /// `accessGroup` is used to identify which Keychain Access Group this entry belongs to. This allows you to use shared keychain access between different applications. 18 | private(set) var accessGroup: String? 19 | 20 | init(serviceName: String, accessGroup: String? = nil) { 21 | self.serviceName = serviceName 22 | self.accessGroup = accessGroup 23 | } 24 | 25 | /// Returns a Data object for a specified key. 26 | /// 27 | /// - parameter forKey: The key to lookup data for. 28 | /// - parameter withAccessibility: Optional accessibility to use when retrieving the keychain item. 29 | /// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. If none is provided, will default to false. 30 | /// - returns: The Data object associated with the key if it exists. If no data exists, returns nil. 31 | func get(forKey key: String, 32 | withAccessibility accessibility: KeychainAccessibility? = nil, 33 | isSynchronizable: Bool = false) -> Data? { 34 | var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable) 35 | keychainQueryDictionary[KeychainHelper.matchLimit] = kSecMatchLimitOne 36 | keychainQueryDictionary[KeychainHelper.returnData] = kCFBooleanTrue 37 | 38 | // Search 39 | var result: AnyObject? 40 | let status = SecItemCopyMatching(keychainQueryDictionary as CFDictionary, &result) 41 | 42 | return status == noErr ? result as? Data : nil 43 | } 44 | 45 | /// Save a Data object to the keychain associated with a specified key. If data already exists for the given key, the data will be overwritten with the new value. 46 | /// 47 | /// - parameter value: The Data object to save. 48 | /// - parameter forKey: The key to save the object under. 49 | /// - parameter withAccessibility: Optional accessibility to use when setting the keychain item. 50 | /// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. If none is provided, will default to false. 51 | /// - returns: True if the save was successful, false otherwise. 52 | @discardableResult 53 | func set(_ value: Data, 54 | forKey key: String, 55 | withAccessibility accessibility: KeychainAccessibility? = nil, 56 | isSynchronizable: Bool = false) -> Bool { 57 | var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable) 58 | keychainQueryDictionary[KeychainHelper.valueData] = value 59 | keychainQueryDictionary[KeychainHelper.attrAccessible] = accessibility?.key ?? KeychainAccessibility.whenUnlocked.key 60 | 61 | let status = SecItemAdd(keychainQueryDictionary as CFDictionary, nil) 62 | if status == errSecSuccess { 63 | return true 64 | } else if status == errSecDuplicateItem { 65 | return update(value, forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable) 66 | } 67 | 68 | return false 69 | } 70 | 71 | /// Remove an object associated with a specified key. If re-using a key but with a different accessibility, first remove the previous key value using removeObjectForKey(:withAccessibility) using the same accessibilty it was saved with. 72 | /// 73 | /// - parameter forKey: The key value to remove data for. 74 | /// - parameter withAccessibility: Optional accessibility level to use when looking up the keychain item. 75 | /// - parameter isSynchronizable: A bool that describes if the item should be synchronizable, to be synched with the iCloud. If none is provided, will default to false. 76 | /// - returns: True if successful, false otherwise. 77 | @discardableResult 78 | func remove(forKey key: String, 79 | withAccessibility accessibility: KeychainAccessibility? = nil, 80 | isSynchronizable: Bool = false) -> Bool { 81 | let keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable) 82 | 83 | let status = SecItemDelete(keychainQueryDictionary as CFDictionary) 84 | return status == errSecSuccess 85 | } 86 | 87 | /// Remove all keychain data added through KeychainWrapper. This will only delete items matching the currnt ServiceName and AccessGroup if one is set. 88 | /// - returns: True if successful, false otherwise. 89 | @discardableResult 90 | func removeAll() -> Bool { 91 | var keychainQueryDictionary: [String: Any] = [KeychainHelper.class: kSecClassGenericPassword] 92 | keychainQueryDictionary[KeychainHelper.attrService] = serviceName 93 | keychainQueryDictionary[KeychainHelper.attrAccessGroup] = accessGroup 94 | 95 | let status = SecItemDelete(keychainQueryDictionary as CFDictionary) 96 | return status == errSecSuccess 97 | } 98 | 99 | // MARK: - Helpers 100 | 101 | private func update(_ value: Data, 102 | forKey key: String, 103 | withAccessibility accessibility: KeychainAccessibility? = nil, 104 | isSynchronizable: Bool = false) -> Bool { 105 | let updateDictionary = [KeychainHelper.valueData: value] 106 | var keychainQueryDictionary = query(forKey: key, withAccessibility: accessibility, isSynchronizable: isSynchronizable) 107 | keychainQueryDictionary[KeychainHelper.attrAccessible] = accessibility?.key 108 | 109 | let status = SecItemUpdate(keychainQueryDictionary as CFDictionary, updateDictionary as CFDictionary) 110 | return status == errSecSuccess 111 | } 112 | 113 | private func query(forKey key: String, 114 | withAccessibility accessibility: KeychainAccessibility? = nil, 115 | isSynchronizable: Bool = false) -> [String: Any] { 116 | var keychainQueryDictionary: [String: Any] = [KeychainHelper.class: kSecClassGenericPassword] 117 | let encodedIdentifier = key.data(using: .utf8) 118 | 119 | keychainQueryDictionary[KeychainHelper.attrService] = serviceName 120 | keychainQueryDictionary[KeychainHelper.attrAccessible] = accessibility?.key 121 | keychainQueryDictionary[KeychainHelper.attrAccessGroup] = accessGroup 122 | keychainQueryDictionary[KeychainHelper.attrGeneric] = encodedIdentifier 123 | keychainQueryDictionary[KeychainHelper.attrAccount] = encodedIdentifier 124 | keychainQueryDictionary[KeychainHelper.attrSynchronizable] = isSynchronizable ? kCFBooleanTrue : kCFBooleanFalse 125 | return keychainQueryDictionary 126 | } 127 | } 128 | 129 | // MARK: - Keys 130 | 131 | extension KeychainHelper { 132 | private static let `class` = kSecClass as String 133 | private static let matchLimit = kSecMatchLimit as String 134 | private static let returnData = kSecReturnData as String 135 | private static let valueData = kSecValueData as String 136 | private static let attrAccessible = kSecAttrAccessible as String 137 | private static let attrService = kSecAttrService as String 138 | private static let attrGeneric = kSecAttrGeneric as String 139 | private static let attrAccount = kSecAttrAccount as String 140 | private static let attrAccessGroup = kSecAttrAccessGroup as String 141 | private static let attrSynchronizable = kSecAttrSynchronizable as String 142 | private static let returnAttributes = kSecReturnAttributes as String 143 | } 144 | 145 | // MARK: - Accessibility 146 | 147 | public enum KeychainAccessibility { 148 | case afterFirstUnlock 149 | case afterFirstUnlockThisDeviceOnly 150 | case whenPasscodeSetThisDeviceOnly 151 | case whenUnlocked 152 | case whenUnlockedThisDeviceOnly 153 | // case always (deprecated) 154 | // case alwaysThisDeviceOnly (deprecated) 155 | 156 | var key: String { 157 | switch self { 158 | case .afterFirstUnlock: return kSecAttrAccessibleAfterFirstUnlock as String 159 | case .afterFirstUnlockThisDeviceOnly: return kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String 160 | case .whenPasscodeSetThisDeviceOnly: return kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly as String 161 | case .whenUnlocked: return kSecAttrAccessibleWhenUnlocked as String 162 | case .whenUnlockedThisDeviceOnly: return kSecAttrAccessibleWhenUnlockedThisDeviceOnly as String 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KeyValueStorage 2 | 3 | ![Build & Test](https://github.com/narek-sv/KeyValueStorage/actions/workflows/swift.yml/badge.svg) 4 | [![Coverage](https://img.shields.io/badge/coverage->=90%25-brightgreen)](https://github.com/narek-sv/KeyValueStorage/actions/workflows/swift.yml) 5 | [![Swift Package Manager compatible](https://img.shields.io/badge/Swift%20Package%20Manager-compatible-success.svg)](https://github.com/apple/swift-package-manager) 6 | [![CocoaPods compatible](https://img.shields.io/cocoapods/v/KeyValueStorageSwift)](https://cocoapods.org/pods/KeyValueStorageSwift) 7 | 8 | --- 9 | 10 | Enhance your development with the state-of-the-art key-value storage framework, meticulously designed for speed, safety, and simplicity. Leveraging Swift's advanced error handling and concurrency features, the framework ensures thread-safe interactions, bolstered by a robust, modular, and protocol-oriented architecture. Unique to the solution, types of values are encoded within the keys, enabling compile-time type inference and eliminating the need for unnecessary casting. It is designed with App Groups in mind, facilitating seamless data sharing between your apps and extensions. Experience a testable, easily integrated storage solution that redefines efficiency and ease of use. 11 | 12 | 13 | --- 14 | ## Supported Platforms 15 | 16 | | | | | | 17 | | --- | --- | --- | --- | 18 | | **iOS** | **macOS** | **watchOS** | **tvOS** | 19 | | 13.0+ | 10.15+ | 6.0+ | 13.0+ | 20 | 21 | ## Built-in Storage Types 22 | 23 | | | | | | 24 | | --- | --- | --- | --- | 25 | | **In Memory** | **User Defaults** | **Keychain** | **File System** | 26 | 27 | --- 28 | ## App Groups 29 | 30 | `KeyValueStorage` also supports working with shared containers, which allows you to share your items among different ***App Extensions*** or ***your other Apps***. To do so, first, you need to configure your app by following the steps described in [this](https://developer.apple.com/documentation/security/keychain_services/keychain_items/sharing_access_to_keychain_items_among_a_collection_of_apps) article. 31 | 32 | By providing corresponding `domain`s to each type of storage, you can enable the sharing of storage spaces. Alternatively, by doing so, you can also keep the containers isolated. 33 | 34 | --- 35 | ## Usage 36 | 37 | The framework is capable of working with any type that conforms to `Codable` and `Sendable`. 38 | The concept here is that first you need to declare the key. It contains every piece of information about how and where the value is stored. 39 | 40 | First, you need to declare the key. You can use one of the built-in types: 41 | 42 | * `UserDefaultsKey` 43 | * `KeychainKey` 44 | * `InMemoryKey` 45 | * `FileKey` 46 | 47 | or you can define your own ones. [See how to do that](#custom-storages) 48 | 49 | ```swift 50 | import KeyValueStorage 51 | 52 | let key = UserDefaultsKey(key: "myKey") 53 | // or alternatively provide the domain 54 | let otherKey = UserDefaultsKey(key: "myKey", domain: "sharedContainer") 55 | ``` 56 | 57 | As you can see, the key holds all the necessary information about the value: 58 | * The key name - `"myKey"` 59 | * The storage type - `UserDefaults` 60 | * The value type - `String` 61 | * The domain (*optional*) - `"sharedContainer"` 62 | 63 | 64 | Now all that is left is to instantiate the storage and use it: 65 | 66 | ```swift 67 | // Instantiate the storage 68 | let storage = UnifiedStorage() 69 | 70 | // Saves the item and associates it with the key, 71 | // or overrides the value if there is already such an item 72 | try await storage.save("Alice", forKey: key) 73 | 74 | // Returns the item associated with the key or returns nil if there is no such item 75 | let value = try await storage.fetch(forKey: key) 76 | 77 | // Deletes the item associated with the key or does nothing if there is no such item 78 | try await storage.delete(forKey: key) 79 | 80 | // Sets the item identified by the key to the provided value 81 | try await storage.set("Bob", forKey: key) // save 82 | try await storage.set(nil, forKey: key) // delete 83 | 84 | // Clears only the storage associated with the specified storage and domain 85 | try await storage.clear(storage: InMemoryStorage.self, forDomain: "someDomain") 86 | 87 | // Clears only the storage associated with the specified storage for all domains 88 | try await storage.clear(storage: InMemoryStorage.self) 89 | 90 | // Clears the whole storage content 91 | try await storage.clear() 92 | ``` 93 | 94 | --- 95 | ## Type Inference 96 | 97 | The framework leverages the full capabilities of ***Swift Generics***, so it can infer the types of values based on the key compile-time, eliminating the need for extra checks or type casting. 98 | 99 | ```swift 100 | struct MyType: Codable, Sendable { ... } 101 | 102 | let key = UserDefaultsKey(key: "myKey") 103 | let value = try await storage.fetch(forKey: key) // inferred type for value is MyType 104 | try await storage.save(/* accepts only MyType*/, forKey: key) 105 | ``` 106 | 107 | --- 108 | ## Custom Storages 109 | 110 | `UnifiedStorage` has 4 built-in storage types: 111 | * `In-memory` - This storage type persists the items only within an app session. 112 | * `User-Defaults` - This storage type persists the items within the app's lifetime. 113 | * `File-System` - This storage saves your key-values as separate files in your file system. 114 | * `Keychain` - This storage type keeps the items in secure storage and persists even after app re-installations. Supports `iCloud` synchronization. 115 | 116 | You can also define your own storage, and it will work with it seamlessly with `UnifiedStorage` out of the box. 117 | All you need to do is: 118 | 1. Define your own type that conforms to the `KeyValueDataStorage` protocol: 119 | ```swift 120 | class NewStorage: KeyValueDataStorage { ... } 121 | ``` 122 | 2. Define the new key type (optional, for ease of use): 123 | ```swift 124 | typealias NewStorageKey = UnifiedStorageKey 125 | ``` 126 | 127 | That's it. You can use it now as the built-in storages: 128 | 129 | ```swift 130 | let key = NewStorageKey(key: customKey) 131 | try await storage.save(UUID(), forKey: key) 132 | ``` 133 | 134 | ***NOTE***! You need to handle the thread safety of your storage on your own. 135 | 136 | --- 137 | ## Xcode autocompletion 138 | 139 | To get the advantages of Xcode autocompletion, it is recommended to declare all your keys in the extension of the `UnifiedStorageKey`, like this: 140 | 141 | ```swift 142 | extension UnifiedStorageKey { 143 | static var key1: UserDefaultsKey { 144 | .init(key: "key1", domain: nil) 145 | } 146 | 147 | static var key2: InMemoryKey { 148 | .init(key: "key2", domain: "sharedContainer") 149 | } 150 | 151 | static var key3: KeychainKey { 152 | .init(key: .init(name: "key3", accessibility: .afterFirstUnlock, isSynchronizable: true), 153 | domain: .init(groupId: "groupId", teamId: "teamId")) 154 | } 155 | 156 | static var key4: FileKey { 157 | .init(key: "key4", domain: "otherContainer") 158 | } 159 | } 160 | ``` 161 | 162 | then Xcode will suggest all the keys specified in the extension when you put a dot: 163 | Screenshot 2024-03-03 at 13 43 39 164 | 165 | --- 166 | ## Keychain 167 | 168 | Use `accessibility` parameter to specify the security level of the keychain storage. 169 | By default the `.whenUnlocked` option is used. It is one of the most restrictive options and provides good data protection. 170 | 171 | You can use `.afterFirstUnlock` if you need your app to access the keychain item while in the background. Note that it is less secure than the `.whenUnlocked` option. 172 | 173 | Here are all the supported accessibility types: 174 | * `afterFirstUnlock` 175 | * `afterFirstUnlockThisDeviceOnly` 176 | * `whenPasscodeSetThisDeviceOnly` 177 | * `whenUnlocked` 178 | * `whenUnlockedThisDeviceOnly` 179 | 180 | Set `synchronizable` property to `true` to enable keychain items synchronization across user's multiple devices. The synchronization will work for users who have the ***Keychain*** enabled in the ***iCloud*** settings on their devices. Deleting a synchronizable item will remove it from all devices. 181 | 182 | ```swift 183 | let key = KeychainKey(key: .init(name: "key", accessibility: .afterFirstUnlock, isSynchronizable: true), 184 | domain: .init(groupId: "groupId", teamId: "teamId")) 185 | ``` 186 | 187 | --- 188 | ## Observation 189 | 190 | The `UnifiedStorage` initializer takes a `factory` parameter that conforms to the `UnifiedStorageFactory` protocol, enabling customized storage instantiation and configuration. This feature is particularly valuable for mocking storage in tests or substituting default implementations with custom ones. 191 | 192 | By default, this parameter is set to `DefaultUnifiedStorageFactory`, which omits observation capabilities to avoid excessive class burden. However, supplying an `ObservableUnifiedStorageFactory` instance as the parameter activates observation of all underlying storages for changes. 193 | 194 | 195 | Combine style publishers: 196 | ```swift 197 | let key = InMemoryKey(key: "key") 198 | guard let publisher = try await storage.publisher(forKey: key) else { 199 | // The storage is not properly configured 200 | return 201 | } 202 | 203 | let subscription = publisher.sink { value in 204 | print(value) // String? 205 | } 206 | ``` 207 | 208 | Concurrency style async streams: 209 | ```swift 210 | guard let stream = try await storage.stream(forKey: key) else { 211 | // The storage is not properly configured 212 | return 213 | } 214 | 215 | for await value in stream { 216 | print(value) // String? 217 | } 218 | ``` 219 | 220 | However, it's important to note that `UnifiedStorage` can only observe changes made through its own methods. 221 | 222 | --- 223 | ## Error handling 224 | 225 | Despite the fact that all the methods of the `UnifiedStorage` are throwing, it will never throw an exception if you do all the initial setups correctly. 226 | 227 | --- 228 | ## Thread Safety 229 | 230 | All built-in types leverage the power of ***Swift Concurrency*** and are thread-safe and protected from race conditions and data racing. However, if you extend the storage with your own ones, it is your responsibility to make them thread-safe. 231 | 232 | --- 233 | ## Tests 234 | 235 | The whole framework is thoroughly validated with high-quality unit tests. 236 | Additionally, it serves as an excellent demonstration of how to use the framework as intended. 237 | 238 | --- 239 | ## Installation 240 | 241 | ### [Swift Package Manager](https://swift.org/package-manager/) 242 | 243 | Once you have your Swift package set up, adding KeyValueStorage as a dependency is as easy as adding it to the `dependencies` value of your `Package.swift`: 244 | 245 | ```swift 246 | dependencies: [ 247 | .package(url: "https://github.com/narek-sv/KeyValueStorage.git", .upToNextMajor(from: "2.0.0")) 248 | ] 249 | ``` 250 | 251 | or 252 | 253 | * In Xcode select *File > Add Packages*. 254 | * Enter the project's URL: https://github.com/narek-sv/KeyValueStorage.git 255 | 256 | In any file you'd like to use the package in, don't forget to 257 | import the framework: 258 | 259 | ```swift 260 | import KeyValueStorage 261 | ``` 262 | 263 | ### [CocoaPods](https://cocoapods.org) 264 | 265 | To integrate KeyValueStorage into your Xcode project using CocoaPods, specify it in your `Podfile`: 266 | 267 | ```ruby 268 | pod 'KeyValueStorageSwift' 269 | ``` 270 | 271 | Then run `pod install`. 272 | 273 | In any file you'd like to use the package in, don't forget to 274 | import the framework: 275 | 276 | ```swift 277 | import KeyValueStorageSwift 278 | ``` 279 | 280 | --- 281 | ## License 282 | 283 | See [License.md](https://github.com/narek-sv/KeyValueStorage/blob/main/LICENSE) for more information. 284 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/KeyValueObservableStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueObservableStorageTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 01.03.24. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | import Combine 11 | @testable import KeyValueStorage 12 | 13 | final class KeyValueObservableStorageTests: XCTestCase { 14 | var underlyingStorage: InMemoryStorage! 15 | var coder: DataCoder! 16 | 17 | @InMemoryActor 18 | override func setUp() async throws { 19 | underlyingStorage = InMemoryStorage() 20 | coder = JSONDataCoder() 21 | InMemoryStorage.container = [nil: [:]] 22 | } 23 | 24 | func testPublisherSameStorageSameDomainSameKey() async throws { 25 | // Given 26 | let operations = [ 27 | ("save", 1), 28 | ("save", 2), 29 | ("delete", nil), 30 | ("delete", nil), 31 | ("set", 4), 32 | ("set", nil), 33 | ("save", 10), 34 | ("clear", nil) 35 | ] 36 | let publisherExpectation = expectation(description: "testPublisherSave") 37 | publisherExpectation.expectedFulfillmentCount = operations.count * 2 38 | 39 | var operationIndex1 = 0 40 | var operationIndex2 = 0 41 | let storage = await KeyValueObservableStorage(storage: InMemoryStorage(), coder: coder) 42 | let key = KeyValueCodingStorageKey(key: "key") 43 | var subscriptions = Set() 44 | await storage.publisher(forKey: key).sink(receiveValue: { 45 | // Then 46 | XCTAssertEqual(operations[operationIndex1].1, $0) 47 | operationIndex1 += 1 48 | publisherExpectation.fulfill() 49 | }).store(in: &subscriptions) 50 | await storage.publisher(forKey: key).sink(receiveValue: { 51 | // Then 52 | XCTAssertEqual(operations[operationIndex2].1, $0) 53 | operationIndex2 += 1 54 | publisherExpectation.fulfill() 55 | }).store(in: &subscriptions) 56 | 57 | // When 58 | for operation in operations { 59 | switch operation { 60 | case let ("save", value): 61 | try await storage.save(value!, forKey: key) 62 | case let ("set", value): 63 | try await storage.set(value, forKey: key) 64 | case ("delete", _): 65 | try await storage.delete(forKey: key) 66 | case ("clear", _): 67 | try await storage.clear() 68 | default: 69 | break 70 | } 71 | } 72 | 73 | await wait(to: [publisherExpectation], timeout: 1) 74 | } 75 | 76 | func testPublisherDifferentStorageSameDomainSameKey1() async throws { 77 | // Given 78 | let operations = [ 79 | ("save", 1, 1), 80 | ("save", 2, 2), 81 | ("delete", nil, 2), 82 | ("delete", nil, 1), 83 | ("set", 4, 2), 84 | ("set", nil, 1), 85 | ("save", 10, 1), 86 | ("clear", nil, 1) 87 | ] 88 | let publisherExpectation = expectation(description: "testPublisherSave") 89 | publisherExpectation.expectedFulfillmentCount = operations.count * 2 90 | 91 | var operationIndex1 = 0 92 | var operationIndex2 = 0 93 | let storage1 = await KeyValueObservableStorage(storage: InMemoryStorage(), coder: coder) 94 | let storage2 = await KeyValueObservableStorage(storage: InMemoryStorage(), coder: coder) 95 | let key = KeyValueCodingStorageKey(key: "key") 96 | var subscriptions = Set() 97 | await storage1.publisher(forKey: key).sink(receiveValue: { 98 | // Then 99 | XCTAssertEqual(operations[operationIndex1].1, $0) 100 | operationIndex1 += 1 101 | publisherExpectation.fulfill() 102 | }).store(in: &subscriptions) 103 | await storage2.publisher(forKey: key).sink(receiveValue: { 104 | // Then 105 | XCTAssertEqual(operations[operationIndex2].1, $0) 106 | operationIndex2 += 1 107 | publisherExpectation.fulfill() 108 | }).store(in: &subscriptions) 109 | 110 | // When 111 | for operation in operations { 112 | let storage = operation.2 == 1 ? storage1 : storage2 113 | switch operation { 114 | case let ("save", value, _): 115 | try await storage.save(value!, forKey: key) 116 | case let ("set", value, _): 117 | try await storage.set(value, forKey: key) 118 | case ("delete", _ , _): 119 | try await storage.delete(forKey: key) 120 | case ("clear", _ , _): 121 | try await storage.clear() 122 | default: 123 | break 124 | } 125 | } 126 | 127 | await wait(to: [publisherExpectation], timeout: 1) 128 | } 129 | 130 | func testPublisherDifferentStorageSameDomainSameKey2() async throws { 131 | // Given 132 | let operations = [ 133 | ("save", 1, 1), 134 | ("save", 2, 2), 135 | ("delete", nil, 2), 136 | ("delete", nil, 1), 137 | ("set", 4, 2), 138 | ("set", nil, 1), 139 | ("save", 10, 1), 140 | ("clear", nil, 1) 141 | ] 142 | let publisherExpectation = expectation(description: "testPublisherSave") 143 | publisherExpectation.expectedFulfillmentCount = operations.count * 2 144 | 145 | var operationIndex1 = 0 146 | var operationIndex2 = 0 147 | let storage1 = await KeyValueObservableStorage(storage: InMemoryStorage(domain: "x"), coder: coder) 148 | let storage2 = await KeyValueObservableStorage(storage: InMemoryStorage(domain: "x"), coder: coder) 149 | let key = KeyValueCodingStorageKey(key: "key") 150 | var subscriptions = Set() 151 | await storage1.publisher(forKey: key).sink(receiveValue: { 152 | // Then 153 | XCTAssertEqual(operations[operationIndex1].1, $0) 154 | operationIndex1 += 1 155 | publisherExpectation.fulfill() 156 | }).store(in: &subscriptions) 157 | await storage2.publisher(forKey: key).sink(receiveValue: { 158 | // Then 159 | XCTAssertEqual(operations[operationIndex2].1, $0) 160 | operationIndex2 += 1 161 | publisherExpectation.fulfill() 162 | }).store(in: &subscriptions) 163 | 164 | // When 165 | for operation in operations { 166 | let storage = operation.2 == 1 ? storage1 : storage2 167 | switch operation { 168 | case let ("save", value, _): 169 | try await storage.save(value!, forKey: key) 170 | case let ("set", value, _): 171 | try await storage.set(value, forKey: key) 172 | case ("delete", _ , _): 173 | try await storage.delete(forKey: key) 174 | case ("clear", _ , _): 175 | try await storage.clear() 176 | default: 177 | break 178 | } 179 | } 180 | 181 | await wait(to: [publisherExpectation], timeout: 1) 182 | } 183 | 184 | func testDifferentDomainsSameKey() async throws { 185 | // Given 186 | let operations1 = [ 187 | ("save", 1), 188 | ("save", 2), 189 | ("delete", nil), 190 | ("delete", nil), 191 | ("set", 4), 192 | ("set", nil), 193 | ("save", 10), 194 | ("clear", nil) 195 | ] 196 | let operations2 = [ 197 | ("delete", nil), 198 | ("save", 6), 199 | ("delete", nil), 200 | ("set", 4), 201 | ("save", 50), 202 | ("clear", nil), 203 | ("save", 32) 204 | ] 205 | let publisherExpectation = expectation(description: "testPublisherSave") 206 | publisherExpectation.expectedFulfillmentCount = operations1.count + operations2.count 207 | 208 | var operationIndex1 = 0 209 | var operationIndex2 = 0 210 | let storage1 = await KeyValueObservableStorage(storage: InMemoryStorage(), coder: coder) 211 | let storage2 = await KeyValueObservableStorage(storage: InMemoryStorage(domain: "other"), coder: coder) 212 | let key = KeyValueCodingStorageKey(key: "key") 213 | var subscriptions = Set() 214 | await storage1.publisher(forKey: key).sink(receiveValue: { 215 | // Then 216 | XCTAssertEqual(operations1[operationIndex1].1, $0) 217 | operationIndex1 += 1 218 | publisherExpectation.fulfill() 219 | }).store(in: &subscriptions) 220 | await storage2.publisher(forKey: key).sink(receiveValue: { 221 | // Then 222 | XCTAssertEqual(operations2[operationIndex2].1, $0) 223 | operationIndex2 += 1 224 | publisherExpectation.fulfill() 225 | }).store(in: &subscriptions) 226 | 227 | // When 228 | for operation in operations1 { 229 | switch operation { 230 | case let ("save", value): 231 | try await storage1.save(value!, forKey: key) 232 | case let ("set", value): 233 | try await storage1.set(value, forKey: key) 234 | case ("delete", _): 235 | try await storage1.delete(forKey: key) 236 | case ("clear", _): 237 | try await storage1.clear() 238 | default: 239 | break 240 | } 241 | } 242 | 243 | for operation in operations2 { 244 | switch operation { 245 | case let ("save", value): 246 | try await storage2.save(value!, forKey: key) 247 | case let ("set", value): 248 | try await storage2.set(value, forKey: key) 249 | case ("delete", _): 250 | try await storage2.delete(forKey: key) 251 | case ("clear", _): 252 | try await storage2.clear() 253 | default: 254 | break 255 | } 256 | } 257 | 258 | await wait(to: [publisherExpectation], timeout: 1) 259 | } 260 | 261 | func testAsyncStream() async throws { 262 | // Given 263 | let operations = [ 264 | ("save", 1), 265 | ("save", 2), 266 | ("delete", nil), 267 | ("delete", nil), 268 | ("set", 4), 269 | ("set", nil), 270 | ("save", 10), 271 | ("clear", nil) 272 | ] 273 | let publisherExpectation = expectation(description: "testPublisherSave") 274 | publisherExpectation.expectedFulfillmentCount = operations.count 275 | 276 | let storage = await KeyValueObservableStorage(storage: InMemoryStorage(), coder: coder) 277 | let key = KeyValueCodingStorageKey(key: "key") 278 | 279 | let task = Task.detached { 280 | var operationIndex = 0 281 | 282 | for await change in await storage.stream(forKey: key) { 283 | XCTAssertEqual(operations[operationIndex].1, change) 284 | operationIndex += 1 285 | publisherExpectation.fulfill() 286 | } 287 | } 288 | 289 | // When 290 | for operation in operations { 291 | switch operation { 292 | case let ("save", value): 293 | print("save") 294 | try await storage.save(value!, forKey: key) 295 | case let ("set", value): 296 | print("set") 297 | try await storage.set(value, forKey: key) 298 | case ("delete", _): 299 | print("delete") 300 | try await storage.delete(forKey: key) 301 | case ("clear", _): 302 | print("clear") 303 | try await storage.clear() 304 | default: 305 | break 306 | } 307 | } 308 | 309 | await wait(to: [publisherExpectation], timeout: 1) 310 | task.cancel() 311 | } 312 | } 313 | 314 | extension XCTestCase { 315 | func wait(to expectations: [XCTestExpectation], timeout: TimeInterval) async { 316 | #if swift(>=5.8) 317 | await fulfillment(of: expectations, timeout: timeout) 318 | #else 319 | wait(for: expectations, timeout: timeout) 320 | #endif 321 | } 322 | } 323 | 324 | 325 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/KeyValueCodingStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueCodingStorageTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 01.03.24. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import KeyValueStorage 11 | 12 | final class KeyValueCodingStorageTests: XCTestCase { 13 | var underlyingStorage: InMemoryStorage! 14 | var storage: KeyValueCodingStorage! 15 | var coder: DataCoder! 16 | 17 | @InMemoryActor 18 | override func setUp() async throws { 19 | underlyingStorage = InMemoryStorage() 20 | coder = JSONDataCoder() 21 | 22 | storage = await .init(storage: underlyingStorage, coder: coder) 23 | 24 | InMemoryStorage.container = [nil: [:]] 25 | } 26 | 27 | @InMemoryActor 28 | func testFetch() async throws { 29 | // Given 30 | let instance1 = CustomStruct(int: 3, string: "3", date: Date(timeIntervalSince1970: 3), inner: [Inner(id: .init())]) 31 | let instance2 = CustomClass(int: 4, string: "4", date: Date(timeIntervalSince1970: 4), inner: [Inner(id: .init())]) 32 | let instance3 = CustomActor(int: 5, string: "4", date: Date(timeIntervalSince1970: 5), inner: [Inner(id: .init())]) 33 | let instance4 = CustomEnum.case1(6) 34 | 35 | let key1 = KeyValueCodingStorageKey(key: "key1") 36 | let key2 = KeyValueCodingStorageKey(key: "key2") 37 | let key3 = KeyValueCodingStorageKey(key: "key3") 38 | let key4 = KeyValueCodingStorageKey(key: "key4") 39 | 40 | // When 41 | var fetched1 = try await storage.fetch(forKey: key1) 42 | var fetched2 = try await storage.fetch(forKey: key2) 43 | var fetched3 = try await storage.fetch(forKey: key3) 44 | var fetched4 = try await storage.fetch(forKey: key4) 45 | 46 | // Then 47 | XCTAssertNil(fetched1) 48 | XCTAssertNil(fetched2) 49 | XCTAssertNil(fetched3) 50 | XCTAssertNil(fetched4) 51 | 52 | // Given 53 | InMemoryStorage.container = [nil: [ 54 | "key1": try await coder.encode(instance1), 55 | "key2": try await coder.encode(instance2), 56 | "key3": try await coder.encode(instance3), 57 | "key4": try await coder.encode(instance4) 58 | ]] 59 | 60 | // When 61 | fetched1 = try await storage.fetch(forKey: key1) 62 | fetched2 = try await storage.fetch(forKey: key2) 63 | fetched3 = try await storage.fetch(forKey: key3) 64 | fetched4 = try await storage.fetch(forKey: key4) 65 | 66 | // Then 67 | XCTAssertEqual(fetched1, instance1) 68 | XCTAssertEqual(fetched2, instance2) 69 | XCTAssertEqual(fetched3, instance3) 70 | XCTAssertEqual(fetched4, instance4) 71 | } 72 | 73 | @InMemoryActor 74 | func testSave() async throws { 75 | // Given 76 | let instance1 = CustomStruct(int: 3, string: "3", date: Date(timeIntervalSince1970: 3), inner: [Inner(id: .init())]) 77 | let instance2 = CustomClass(int: 4, string: "4", date: Date(timeIntervalSince1970: 4), inner: [Inner(id: .init())]) 78 | let instance3 = CustomActor(int: 5, string: "4", date: Date(timeIntervalSince1970: 5), inner: [Inner(id: .init())]) 79 | let instance4 = CustomEnum.case1(6) 80 | 81 | let key1 = KeyValueCodingStorageKey(key: "key1") 82 | let key2 = KeyValueCodingStorageKey(key: "key2") 83 | let key3 = KeyValueCodingStorageKey(key: "key3") 84 | let key4 = KeyValueCodingStorageKey(key: "key4") 85 | 86 | // When 87 | try await storage.save(instance1, forKey: key1) 88 | try await storage.save(instance2, forKey: key2) 89 | try await storage.save(instance3, forKey: key3) 90 | try await storage.save(instance4, forKey: key4) 91 | 92 | // Then 93 | let decoded1: CustomStruct = try await coder.decode(InMemoryStorage.container[nil]!["key1"]!) 94 | let decoded2: CustomClass = try await coder.decode(InMemoryStorage.container[nil]!["key2"]!) 95 | let decoded3: CustomActor = try await coder.decode(InMemoryStorage.container[nil]!["key3"]!) 96 | let decoded4: CustomEnum = try await coder.decode(InMemoryStorage.container[nil]!["key4"]!) 97 | 98 | XCTAssertEqual(decoded1, instance1) 99 | XCTAssertEqual(decoded2, instance2) 100 | XCTAssertEqual(decoded3, instance3) 101 | XCTAssertEqual(decoded4, instance4) 102 | } 103 | 104 | @InMemoryActor 105 | func testDelete() async throws { 106 | // Given 107 | let instance1 = CustomStruct(int: 3, string: "3", date: Date(timeIntervalSince1970: 3), inner: [Inner(id: .init())]) 108 | let instance2 = CustomClass(int: 4, string: "4", date: Date(timeIntervalSince1970: 4), inner: [Inner(id: .init())]) 109 | let instance3 = CustomActor(int: 5, string: "4", date: Date(timeIntervalSince1970: 5), inner: [Inner(id: .init())]) 110 | let instance4 = CustomEnum.case1(6) 111 | 112 | let key1 = KeyValueCodingStorageKey(key: "key1") 113 | let key2 = KeyValueCodingStorageKey(key: "key2") 114 | let key3 = KeyValueCodingStorageKey(key: "key3") 115 | let key4 = KeyValueCodingStorageKey(key: "key4") 116 | 117 | InMemoryStorage.container = [nil: [ 118 | "key1": try await coder.encode(instance1), 119 | "key2": try await coder.encode(instance2), 120 | "key3": try await coder.encode(instance3), 121 | "key4": try await coder.encode(instance4) 122 | ]] 123 | 124 | // When 125 | try await storage.delete(forKey: key1) 126 | try await storage.delete(forKey: key2) 127 | try await storage.delete(forKey: key3) 128 | try await storage.delete(forKey: key4) 129 | 130 | // Then 131 | XCTAssertNil(InMemoryStorage.container[nil]?["key1"]) 132 | XCTAssertNil(InMemoryStorage.container[nil]?["key2"]) 133 | XCTAssertNil(InMemoryStorage.container[nil]?["key3"]) 134 | XCTAssertNil(InMemoryStorage.container[nil]?["key4"]) 135 | } 136 | 137 | @InMemoryActor 138 | func testSet() async throws { 139 | // Given 140 | let instance1 = CustomStruct(int: 3, string: "3", date: Date(timeIntervalSince1970: 3), inner: [Inner(id: .init())]) 141 | let instance2 = CustomClass(int: 4, string: "4", date: Date(timeIntervalSince1970: 4), inner: [Inner(id: .init())]) 142 | let instance3 = CustomActor(int: 5, string: "4", date: Date(timeIntervalSince1970: 5), inner: [Inner(id: .init())]) 143 | let instance4 = CustomEnum.case1(6) 144 | 145 | let key1 = KeyValueCodingStorageKey(key: "key1") 146 | let key2 = KeyValueCodingStorageKey(key: "key2") 147 | let key3 = KeyValueCodingStorageKey(key: "key3") 148 | let key4 = KeyValueCodingStorageKey(key: "key4") 149 | 150 | InMemoryStorage.container = [nil: [ 151 | "key1": try await coder.encode(instance1), 152 | "key2": try await coder.encode(instance2), 153 | "key3": try await coder.encode(instance3), 154 | "key4": try await coder.encode(instance4) 155 | ]] 156 | 157 | // When 158 | try await storage.set(nil, forKey: key1) 159 | try await storage.set(nil, forKey: key2) 160 | try await storage.set(nil, forKey: key3) 161 | try await storage.set(nil, forKey: key4) 162 | 163 | // Then 164 | XCTAssertNil(InMemoryStorage.container[nil]?["key1"]) 165 | XCTAssertNil(InMemoryStorage.container[nil]?["key2"]) 166 | XCTAssertNil(InMemoryStorage.container[nil]?["key3"]) 167 | XCTAssertNil(InMemoryStorage.container[nil]?["key4"]) 168 | 169 | // When 170 | try await storage.set(instance1, forKey: key1) 171 | try await storage.set(instance2, forKey: key2) 172 | try await storage.set(instance3, forKey: key3) 173 | try await storage.set(instance4, forKey: key4) 174 | 175 | // Then 176 | let decoded1: CustomStruct = try await coder.decode(InMemoryStorage.container[nil]!["key1"]!) 177 | let decoded2: CustomClass = try await coder.decode(InMemoryStorage.container[nil]!["key2"]!) 178 | let decoded3: CustomActor = try await coder.decode(InMemoryStorage.container[nil]!["key3"]!) 179 | let decoded4: CustomEnum = try await coder.decode(InMemoryStorage.container[nil]!["key4"]!) 180 | 181 | XCTAssertEqual(decoded1, instance1) 182 | XCTAssertEqual(decoded2, instance2) 183 | XCTAssertEqual(decoded3, instance3) 184 | XCTAssertEqual(decoded4, instance4) 185 | } 186 | 187 | @InMemoryActor 188 | func testClear() async throws { 189 | // Given 190 | let instance1 = CustomStruct(int: 3, string: "3", date: Date(timeIntervalSince1970: 3), inner: [Inner(id: .init())]) 191 | let instance2 = CustomClass(int: 4, string: "4", date: Date(timeIntervalSince1970: 4), inner: [Inner(id: .init())]) 192 | let instance3 = CustomActor(int: 5, string: "4", date: Date(timeIntervalSince1970: 5), inner: [Inner(id: .init())]) 193 | let instance4 = CustomEnum.case1(6) 194 | 195 | InMemoryStorage.container = [nil: [ 196 | "key1": try await coder.encode(instance1), 197 | "key2": try await coder.encode(instance2), 198 | "key3": try await coder.encode(instance3), 199 | "key4": try await coder.encode(instance4) 200 | ]] 201 | 202 | // When 203 | try await storage.clear() 204 | 205 | // Then 206 | XCTAssertNil(InMemoryStorage.container[nil]?["key1"]) 207 | XCTAssertNil(InMemoryStorage.container[nil]?["key2"]) 208 | XCTAssertNil(InMemoryStorage.container[nil]?["key3"]) 209 | XCTAssertNil(InMemoryStorage.container[nil]?["key4"]) 210 | } 211 | 212 | } 213 | 214 | extension KeyValueCodingStorageTests { 215 | struct CustomStruct: Codable, Equatable { 216 | let int: Int 217 | let string: String 218 | let date: Date 219 | let inner: [Inner] 220 | } 221 | 222 | class CustomClass: Codable, Equatable { 223 | let int: Int 224 | let string: String 225 | let date: Date 226 | let inner: [Inner] 227 | 228 | init(int: Int, string: String, date: Date, inner: [Inner]) { 229 | self.int = int 230 | self.string = string 231 | self.date = date 232 | self.inner = inner 233 | } 234 | 235 | static func == (lhs: CustomClass, rhs: CustomClass) -> Bool { 236 | lhs.int == rhs.int && 237 | lhs.string == rhs.string && 238 | lhs.date == rhs.date && 239 | lhs.inner == rhs.inner 240 | } 241 | } 242 | 243 | actor CustomActor: Codable, Equatable { 244 | let int: Int 245 | let string: String 246 | let date: Date 247 | let inner: [Inner] 248 | 249 | init(int: Int, string: String, date: Date, inner: [Inner]) { 250 | self.int = int 251 | self.string = string 252 | self.date = date 253 | self.inner = inner 254 | } 255 | 256 | static func == (lhs: CustomActor, rhs: CustomActor) -> Bool { 257 | lhs.int == rhs.int && 258 | lhs.string == rhs.string && 259 | lhs.date == rhs.date && 260 | lhs.inner == rhs.inner 261 | } 262 | 263 | init(from decoder: Decoder) throws { 264 | let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) 265 | self.int = try container.decode(Int.self, forKey: .int) 266 | self.string = try container.decode(String.self, forKey: .string) 267 | self.date = try container.decode(Date.self, forKey: .date) 268 | self.inner = try container.decode([Inner].self, forKey: .inner) 269 | } 270 | 271 | nonisolated func encode(to encoder: Encoder) throws { 272 | var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) 273 | try container.encode(self.int, forKey: .int) 274 | try container.encode(self.string, forKey: .string) 275 | try container.encode(self.date, forKey: .date) 276 | try container.encode(self.inner, forKey: .inner) 277 | } 278 | 279 | enum CodingKeys: CodingKey { 280 | case int 281 | case string 282 | case date 283 | case inner 284 | } 285 | } 286 | 287 | enum CustomEnum: Codable, Equatable { 288 | case case1(Int) 289 | case case2(String) 290 | case case3(date: Date) 291 | } 292 | 293 | struct Inner: Codable, Equatable { 294 | let id: UUID 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/UnifiedStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnifiedStorageTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 12.12.23. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import KeyValueStorage 11 | 12 | final class UnifiedStorageTests: XCTestCase { 13 | 14 | override func setUp() async throws { 15 | await InMemoryStorage().clear() 16 | await InMemoryStorage(domain: "other").clear() 17 | 18 | await UserDefaultsStorage().clear() 19 | try await UserDefaultsStorage(domain: "other").clear() 20 | } 21 | 22 | func testStorage() async throws { 23 | // Given 24 | let key1 = InMemoryKey(key: "key1") 25 | let key2 = InMemoryKey(key: "key2") 26 | let key3 = InMemoryKey(key: "key3", domain: "other") 27 | let key4 = InMemoryKey(key: "key4", domain: "other") 28 | let key5 = UserDefaultsKey(key: "key1") 29 | let key6 = UserDefaultsKey(key: "key2") 30 | let key7 = UserDefaultsKey(key: "key3", domain: "other") 31 | let key8 = UserDefaultsKey(key: "key4", domain: "other") 32 | let storage = UnifiedStorage() 33 | 34 | // When 35 | var fetched1 = try await storage.fetch(forKey: key1) 36 | var fetched2 = try await storage.fetch(forKey: key2) 37 | var fetched3 = try await storage.fetch(forKey: key3) 38 | var fetched4 = try await storage.fetch(forKey: key4) 39 | var fetched5 = try await storage.fetch(forKey: key5) 40 | var fetched6 = try await storage.fetch(forKey: key6) 41 | var fetched7 = try await storage.fetch(forKey: key7) 42 | var fetched8 = try await storage.fetch(forKey: key8) 43 | 44 | // Then 45 | XCTAssertNil(fetched1) 46 | XCTAssertNil(fetched2) 47 | XCTAssertNil(fetched3) 48 | XCTAssertNil(fetched4) 49 | XCTAssertNil(fetched5) 50 | XCTAssertNil(fetched6) 51 | XCTAssertNil(fetched7) 52 | XCTAssertNil(fetched8) 53 | 54 | // When 55 | try await storage.save("data1", forKey: key1) 56 | try await storage.save("data2", forKey: key2) 57 | try await storage.save("data3", forKey: key3) 58 | try await storage.save("data4", forKey: key4) 59 | try await storage.save("data5", forKey: key5) 60 | try await storage.save("data6", forKey: key6) 61 | try await storage.save("data7", forKey: key7) 62 | try await storage.save("data8", forKey: key8) 63 | 64 | fetched1 = try await storage.fetch(forKey: key1) 65 | fetched2 = try await storage.fetch(forKey: key2) 66 | fetched3 = try await storage.fetch(forKey: key3) 67 | fetched4 = try await storage.fetch(forKey: key4) 68 | fetched5 = try await storage.fetch(forKey: key5) 69 | fetched6 = try await storage.fetch(forKey: key6) 70 | fetched7 = try await storage.fetch(forKey: key7) 71 | fetched8 = try await storage.fetch(forKey: key8) 72 | 73 | // Then 74 | XCTAssertEqual(fetched1, "data1") 75 | XCTAssertEqual(fetched2, "data2") 76 | XCTAssertEqual(fetched3, "data3") 77 | XCTAssertEqual(fetched4, "data4") 78 | XCTAssertEqual(fetched5, "data5") 79 | XCTAssertEqual(fetched6, "data6") 80 | XCTAssertEqual(fetched7, "data7") 81 | XCTAssertEqual(fetched8, "data8") 82 | 83 | // When 84 | try await storage.delete(forKey: key1) 85 | try await storage.delete(forKey: key2) 86 | try await storage.delete(forKey: key3) 87 | try await storage.delete(forKey: key4) 88 | try await storage.delete(forKey: key5) 89 | try await storage.delete(forKey: key6) 90 | try await storage.delete(forKey: key7) 91 | try await storage.delete(forKey: key8) 92 | 93 | fetched1 = try await storage.fetch(forKey: key1) 94 | fetched2 = try await storage.fetch(forKey: key2) 95 | fetched3 = try await storage.fetch(forKey: key3) 96 | fetched4 = try await storage.fetch(forKey: key4) 97 | fetched5 = try await storage.fetch(forKey: key5) 98 | fetched6 = try await storage.fetch(forKey: key6) 99 | fetched7 = try await storage.fetch(forKey: key7) 100 | fetched8 = try await storage.fetch(forKey: key8) 101 | 102 | // Then 103 | XCTAssertNil(fetched1) 104 | XCTAssertNil(fetched2) 105 | XCTAssertNil(fetched3) 106 | XCTAssertNil(fetched4) 107 | XCTAssertNil(fetched5) 108 | XCTAssertNil(fetched6) 109 | XCTAssertNil(fetched7) 110 | XCTAssertNil(fetched8) 111 | 112 | // When 113 | try await storage.set("newData1", forKey: key1) 114 | try await storage.set("newData2", forKey: key2) 115 | try await storage.set("newData3", forKey: key3) 116 | try await storage.set("newData4", forKey: key4) 117 | try await storage.set("newData5", forKey: key5) 118 | try await storage.set("newData6", forKey: key6) 119 | try await storage.set("newData7", forKey: key7) 120 | try await storage.set("newData8", forKey: key8) 121 | 122 | fetched1 = try await storage.fetch(forKey: key1) 123 | fetched2 = try await storage.fetch(forKey: key2) 124 | fetched3 = try await storage.fetch(forKey: key3) 125 | fetched4 = try await storage.fetch(forKey: key4) 126 | fetched5 = try await storage.fetch(forKey: key5) 127 | fetched6 = try await storage.fetch(forKey: key6) 128 | fetched7 = try await storage.fetch(forKey: key7) 129 | fetched8 = try await storage.fetch(forKey: key8) 130 | 131 | // Then 132 | XCTAssertEqual(fetched1, "newData1") 133 | XCTAssertEqual(fetched2, "newData2") 134 | XCTAssertEqual(fetched3, "newData3") 135 | XCTAssertEqual(fetched4, "newData4") 136 | XCTAssertEqual(fetched5, "newData5") 137 | XCTAssertEqual(fetched6, "newData6") 138 | XCTAssertEqual(fetched7, "newData7") 139 | XCTAssertEqual(fetched8, "newData8") 140 | 141 | // When 142 | try await storage.set(nil, forKey: key1) 143 | try await storage.set(nil, forKey: key2) 144 | try await storage.set(nil, forKey: key3) 145 | try await storage.set(nil, forKey: key4) 146 | try await storage.set(nil, forKey: key5) 147 | try await storage.set(nil, forKey: key6) 148 | try await storage.set(nil, forKey: key7) 149 | try await storage.set(nil, forKey: key8) 150 | 151 | fetched1 = try await storage.fetch(forKey: key1) 152 | fetched2 = try await storage.fetch(forKey: key2) 153 | fetched3 = try await storage.fetch(forKey: key3) 154 | fetched4 = try await storage.fetch(forKey: key4) 155 | fetched5 = try await storage.fetch(forKey: key5) 156 | fetched6 = try await storage.fetch(forKey: key6) 157 | fetched7 = try await storage.fetch(forKey: key7) 158 | fetched8 = try await storage.fetch(forKey: key8) 159 | 160 | // Then 161 | XCTAssertNil(fetched1) 162 | XCTAssertNil(fetched2) 163 | XCTAssertNil(fetched3) 164 | XCTAssertNil(fetched4) 165 | XCTAssertNil(fetched5) 166 | XCTAssertNil(fetched6) 167 | XCTAssertNil(fetched7) 168 | XCTAssertNil(fetched8) 169 | 170 | // When 171 | try await storage.save("newData1", forKey: key1) 172 | try await storage.save("newData2", forKey: key2) 173 | try await storage.save("newData3", forKey: key3) 174 | try await storage.save("newData4", forKey: key4) 175 | try await storage.save("newData5", forKey: key5) 176 | try await storage.save("newData6", forKey: key6) 177 | try await storage.save("newData7", forKey: key7) 178 | try await storage.save("newData8", forKey: key8) 179 | 180 | try await storage.clear(storage: KeychainStorage.self) 181 | 182 | fetched1 = try await storage.fetch(forKey: key1) 183 | fetched2 = try await storage.fetch(forKey: key2) 184 | fetched3 = try await storage.fetch(forKey: key3) 185 | fetched4 = try await storage.fetch(forKey: key4) 186 | fetched5 = try await storage.fetch(forKey: key5) 187 | fetched6 = try await storage.fetch(forKey: key6) 188 | fetched7 = try await storage.fetch(forKey: key7) 189 | fetched8 = try await storage.fetch(forKey: key8) 190 | 191 | // Then 192 | XCTAssertEqual(fetched1, "newData1") 193 | XCTAssertEqual(fetched2, "newData2") 194 | XCTAssertEqual(fetched3, "newData3") 195 | XCTAssertEqual(fetched4, "newData4") 196 | XCTAssertEqual(fetched5, "newData5") 197 | XCTAssertEqual(fetched6, "newData6") 198 | XCTAssertEqual(fetched7, "newData7") 199 | XCTAssertEqual(fetched8, "newData8") 200 | 201 | // When 202 | try await storage.clear(storage: InMemoryStorage.self, forDomain: nil) 203 | 204 | fetched1 = try await storage.fetch(forKey: key1) 205 | fetched2 = try await storage.fetch(forKey: key2) 206 | fetched3 = try await storage.fetch(forKey: key3) 207 | fetched4 = try await storage.fetch(forKey: key4) 208 | fetched5 = try await storage.fetch(forKey: key5) 209 | fetched6 = try await storage.fetch(forKey: key6) 210 | fetched7 = try await storage.fetch(forKey: key7) 211 | fetched8 = try await storage.fetch(forKey: key8) 212 | 213 | // Then 214 | XCTAssertNil(fetched1) 215 | XCTAssertNil(fetched2) 216 | XCTAssertEqual(fetched3, "newData3") 217 | XCTAssertEqual(fetched4, "newData4") 218 | XCTAssertEqual(fetched5, "newData5") 219 | XCTAssertEqual(fetched6, "newData6") 220 | XCTAssertEqual(fetched7, "newData7") 221 | XCTAssertEqual(fetched8, "newData8") 222 | 223 | // When 224 | try await storage.clear(storage: InMemoryStorage.self) 225 | 226 | fetched1 = try await storage.fetch(forKey: key1) 227 | fetched2 = try await storage.fetch(forKey: key2) 228 | fetched3 = try await storage.fetch(forKey: key3) 229 | fetched4 = try await storage.fetch(forKey: key4) 230 | fetched5 = try await storage.fetch(forKey: key5) 231 | fetched6 = try await storage.fetch(forKey: key6) 232 | fetched7 = try await storage.fetch(forKey: key7) 233 | fetched8 = try await storage.fetch(forKey: key8) 234 | 235 | // Then 236 | XCTAssertNil(fetched1) 237 | XCTAssertNil(fetched2) 238 | XCTAssertNil(fetched3) 239 | XCTAssertNil(fetched4) 240 | XCTAssertEqual(fetched5, "newData5") 241 | XCTAssertEqual(fetched6, "newData6") 242 | XCTAssertEqual(fetched7, "newData7") 243 | XCTAssertEqual(fetched8, "newData8") 244 | 245 | // When 246 | try await storage.clear() 247 | 248 | fetched1 = try await storage.fetch(forKey: key1) 249 | fetched2 = try await storage.fetch(forKey: key2) 250 | fetched3 = try await storage.fetch(forKey: key3) 251 | fetched4 = try await storage.fetch(forKey: key4) 252 | fetched5 = try await storage.fetch(forKey: key5) 253 | fetched6 = try await storage.fetch(forKey: key6) 254 | fetched7 = try await storage.fetch(forKey: key7) 255 | fetched8 = try await storage.fetch(forKey: key8) 256 | 257 | // Then 258 | XCTAssertNil(fetched1) 259 | XCTAssertNil(fetched2) 260 | XCTAssertNil(fetched3) 261 | XCTAssertNil(fetched4) 262 | XCTAssertNil(fetched5) 263 | XCTAssertNil(fetched6) 264 | XCTAssertNil(fetched7) 265 | XCTAssertNil(fetched8) 266 | } 267 | 268 | func testNonObservable() async throws { 269 | // Given 270 | var stoarge = UnifiedStorage(factory: DefaultUnifiedStorageFactory()) 271 | 272 | // When 273 | var publisher = try await stoarge.publisher(forKey: InMemoryKey(key: "key")) 274 | var stream = try await stoarge.stream(forKey: InMemoryKey(key: "key")) 275 | 276 | // Then 277 | XCTAssertNil(publisher) 278 | XCTAssertNil(stream) 279 | 280 | // Given 281 | stoarge = UnifiedStorage(factory: ObservableUnifiedStorageFactory()) 282 | 283 | // When 284 | publisher = try await stoarge.publisher(forKey: InMemoryKey(key: "key")) 285 | stream = try await stoarge.stream(forKey: InMemoryKey(key: "key")) 286 | 287 | // Then 288 | XCTAssertNotNil(publisher) 289 | XCTAssertNotNil(stream) 290 | } 291 | } 292 | 293 | final class MockedUnifiedStorageFactory: DefaultUnifiedStorageFactory { 294 | override func dataStorage(for domain: Storage.Domain?) async throws -> Storage { 295 | switch Storage.self { 296 | case is InMemoryMock.Type: 297 | if let domain = domain as? InMemoryStorage.Domain { 298 | return await InMemoryMock(domain: domain) as! Storage 299 | } else { 300 | return await InMemoryMock() as! Storage 301 | } 302 | default: 303 | return try await super.dataStorage(for: domain) 304 | } 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/DataStorageTests/InMemoryStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InMemoryStorageTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 31.12.23. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import KeyValueStorage 11 | 12 | final class InMemoryStorageTests: XCTestCase { 13 | static let otherStorageDomain = "other" 14 | var standardStorage: InMemoryStorage! 15 | var otherStorage: InMemoryStorage! 16 | 17 | @InMemoryActor 18 | override func setUp() { 19 | standardStorage = InMemoryStorage() 20 | otherStorage = InMemoryStorage(domain: Self.otherStorageDomain) 21 | 22 | InMemoryStorage.container = [:] 23 | } 24 | 25 | @InMemoryActor 26 | func testInMemoryDomain() { 27 | XCTAssertEqual(standardStorage.domain, nil) 28 | XCTAssertEqual(otherStorage.domain, Self.otherStorageDomain) 29 | } 30 | 31 | @InMemoryActor 32 | func testInMemoryFetch() { 33 | // Given 34 | let data1 = Data([0xAA, 0xBB, 0xCC]) 35 | let data2 = Data([0xDD, 0xEE, 0xFF]) 36 | let key1 = "key1" 37 | let key2 = "key2" 38 | 39 | // When 40 | var fetched1 = standardStorage.fetch(forKey: key1) 41 | var fetched2 = standardStorage.fetch(forKey: key2) 42 | 43 | // Then 44 | XCTAssertNil(fetched1) 45 | XCTAssertNil(fetched2) 46 | 47 | // When 48 | InMemoryStorage.container = [nil: [key1: data1]] 49 | fetched1 = standardStorage.fetch(forKey: key1) 50 | fetched2 = standardStorage.fetch(forKey: key2) 51 | 52 | // Then 53 | XCTAssertEqual(fetched1, data1) 54 | XCTAssertNil(fetched2) 55 | 56 | // When 57 | InMemoryStorage.container = [nil: [key1: data2]] 58 | fetched1 = standardStorage.fetch(forKey: key1) 59 | fetched2 = standardStorage.fetch(forKey: key2) 60 | 61 | // Then 62 | XCTAssertEqual(fetched1, data2) 63 | XCTAssertNil(fetched2) 64 | 65 | // When 66 | InMemoryStorage.container = [nil: [key1: data1, key2: data2]] 67 | fetched1 = standardStorage.fetch(forKey: key1) 68 | fetched2 = standardStorage.fetch(forKey: key2) 69 | 70 | // Then 71 | XCTAssertEqual(fetched1, data1) 72 | XCTAssertEqual(fetched2, data2) 73 | 74 | // When 75 | InMemoryStorage.container = [nil: [key2: data1]] 76 | fetched1 = standardStorage.fetch(forKey: key1) 77 | fetched2 = standardStorage.fetch(forKey: key2) 78 | 79 | // Then 80 | XCTAssertNil(fetched1) 81 | XCTAssertEqual(fetched2, data1) 82 | 83 | // When 84 | InMemoryStorage.container = [nil: [:]] 85 | fetched1 = standardStorage.fetch(forKey: key1) 86 | fetched2 = standardStorage.fetch(forKey: key2) 87 | 88 | // Then 89 | XCTAssertNil(fetched1) 90 | XCTAssertNil(fetched2) 91 | } 92 | 93 | @InMemoryActor 94 | func testInMemoryFetchDifferentDomains() { 95 | // Given 96 | let data1 = Data([0xAA, 0xBB, 0xCC]) 97 | let data2 = Data([0xDD, 0xEE, 0xFF]) 98 | let key = "key" 99 | 100 | // When 101 | var fetched1 = standardStorage.fetch(forKey: key) 102 | var fetched2 = otherStorage.fetch(forKey: key) 103 | 104 | // Then 105 | XCTAssertNil(fetched1) 106 | XCTAssertNil(fetched2) 107 | 108 | // When 109 | InMemoryStorage.container = [nil: [key: data1]] 110 | fetched1 = standardStorage.fetch(forKey: key) 111 | fetched2 = otherStorage.fetch(forKey: key) 112 | 113 | // Then 114 | XCTAssertEqual(fetched1, data1) 115 | XCTAssertNil(fetched2) 116 | 117 | // When 118 | InMemoryStorage.container = ["other": [key: data2]] 119 | fetched1 = standardStorage.fetch(forKey: key) 120 | fetched2 = otherStorage.fetch(forKey: key) 121 | 122 | // Then 123 | XCTAssertNil(fetched1) 124 | XCTAssertEqual(fetched2, data2) 125 | 126 | // When 127 | InMemoryStorage.container = [nil: [key: data2]] 128 | fetched1 = standardStorage.fetch(forKey: key) 129 | fetched2 = otherStorage.fetch(forKey: key) 130 | 131 | // Then 132 | XCTAssertEqual(fetched1, data2) 133 | XCTAssertNil(fetched2) 134 | 135 | // When 136 | InMemoryStorage.container = [nil: [key: data1], "other": [key: data2]] 137 | fetched1 = standardStorage.fetch(forKey: key) 138 | fetched2 = otherStorage.fetch(forKey: key) 139 | 140 | // Then 141 | XCTAssertEqual(fetched1, data1) 142 | XCTAssertEqual(fetched2, data2) 143 | 144 | // When 145 | InMemoryStorage.container = [nil: [:], "other": [:]] 146 | fetched1 = standardStorage.fetch(forKey: key) 147 | fetched2 = otherStorage.fetch(forKey: key) 148 | 149 | // Then 150 | XCTAssertNil(fetched1) 151 | XCTAssertNil(fetched2) 152 | } 153 | 154 | @InMemoryActor 155 | func testInMemorySave() { 156 | // Given 157 | let data1 = Data([0xAA, 0xBB, 0xCC]) 158 | let data2 = Data([0xDD, 0xEE, 0xFF]) 159 | let key1 = "key1" 160 | let key2 = "key2" 161 | 162 | // When 163 | standardStorage.save(data1, forKey: key1) 164 | 165 | // Then 166 | XCTAssertEqual(InMemoryStorage.container[nil]?[key1], data1) 167 | XCTAssertNil(InMemoryStorage.container[nil]?[key2]) 168 | 169 | // When 170 | standardStorage.save(data2, forKey: key1) 171 | 172 | // Then 173 | XCTAssertEqual(InMemoryStorage.container[nil]?[key1], data2) 174 | XCTAssertNil(InMemoryStorage.container[nil]?[key2]) 175 | 176 | // When 177 | standardStorage.save(data1, forKey: key2) 178 | 179 | // Then 180 | XCTAssertEqual(InMemoryStorage.container[nil]?[key1], data2) 181 | XCTAssertEqual(InMemoryStorage.container[nil]?[key2], data1) 182 | } 183 | 184 | @InMemoryActor 185 | func testInMemorySaveDifferentDomains() { 186 | // Given 187 | let data1 = Data([0xAA, 0xBB, 0xCC]) 188 | let data2 = Data([0xDD, 0xEE, 0xFF]) 189 | let key = "key" 190 | 191 | // When 192 | standardStorage.save(data1, forKey: key) 193 | 194 | // Then 195 | XCTAssertEqual(InMemoryStorage.container[nil]?[key], data1) 196 | XCTAssertNil(InMemoryStorage.container["other"]?[key]) 197 | 198 | // When 199 | otherStorage.save(data2, forKey: key) 200 | 201 | // Then 202 | XCTAssertEqual(InMemoryStorage.container[nil]?[key], data1) 203 | XCTAssertEqual(InMemoryStorage.container["other"]?[key], data2) 204 | 205 | // When 206 | standardStorage.save(data2, forKey: key) 207 | 208 | // Then 209 | XCTAssertEqual(InMemoryStorage.container[nil]?[key], data2) 210 | XCTAssertEqual(InMemoryStorage.container["other"]?[key], data2) 211 | } 212 | 213 | @InMemoryActor 214 | func testInMemoryDelete() { 215 | // Given 216 | let data1 = Data([0xAA, 0xBB, 0xCC]) 217 | let data2 = Data([0xDD, 0xEE, 0xFF]) 218 | let key1 = "key1" 219 | let key2 = "key2" 220 | InMemoryStorage.container = [nil: [key1: data1, key2: data2]] 221 | 222 | // When 223 | standardStorage.delete(forKey: key1) 224 | 225 | // Then 226 | XCTAssertNil(InMemoryStorage.container[nil]?[key1]) 227 | XCTAssertEqual(InMemoryStorage.container[nil]?[key2], data2) 228 | 229 | // When 230 | standardStorage.delete(forKey: key1) 231 | 232 | // Then 233 | XCTAssertNil(InMemoryStorage.container[nil]?[key1]) 234 | XCTAssertEqual(InMemoryStorage.container[nil]?[key2], data2) 235 | 236 | // When 237 | standardStorage.delete(forKey: key2) 238 | 239 | // Then 240 | XCTAssertNil(InMemoryStorage.container[nil]?[key1]) 241 | XCTAssertNil(InMemoryStorage.container[nil]?[key2]) 242 | } 243 | 244 | @InMemoryActor 245 | func testInMemoryDeleteDifferentDomains() { 246 | // Given 247 | let data1 = Data([0xAA, 0xBB, 0xCC]) 248 | let data2 = Data([0xDD, 0xEE, 0xFF]) 249 | let key = "key" 250 | InMemoryStorage.container = [nil: [key: data1], "other": [key: data2]] 251 | 252 | // When 253 | standardStorage.delete(forKey: key) 254 | 255 | // Then 256 | XCTAssertNil(InMemoryStorage.container[nil]?[key]) 257 | XCTAssertEqual(InMemoryStorage.container["other"]?[key], data2) 258 | 259 | // When 260 | standardStorage.delete(forKey: key) 261 | 262 | // Then 263 | XCTAssertNil(InMemoryStorage.container[nil]?[key]) 264 | XCTAssertEqual(InMemoryStorage.container["other"]?[key], data2) 265 | 266 | // When 267 | otherStorage.delete(forKey: key) 268 | 269 | // Then 270 | XCTAssertNil(InMemoryStorage.container[nil]?[key]) 271 | XCTAssertNil(InMemoryStorage.container["other"]?[key]) 272 | } 273 | 274 | @InMemoryActor 275 | func testInMemorySet() { 276 | // Given 277 | let data1 = Data([0xAA, 0xBB, 0xCC]) 278 | let data2 = Data([0xDD, 0xEE, 0xFF]) 279 | let key1 = "key1" 280 | let key2 = "key2" 281 | InMemoryStorage.container = [nil: [key1: data1, key2: data2]] 282 | 283 | // When 284 | standardStorage.set(data2, forKey: key1) 285 | 286 | // Then 287 | XCTAssertEqual(InMemoryStorage.container[nil]?[key1], data2) 288 | XCTAssertEqual(InMemoryStorage.container[nil]?[key2], data2) 289 | 290 | // When 291 | standardStorage.set(nil, forKey: key1) 292 | 293 | // Then 294 | XCTAssertNil(InMemoryStorage.container[nil]?[key1]) 295 | XCTAssertEqual(InMemoryStorage.container[nil]?[key2], data2) 296 | 297 | // When 298 | standardStorage.set(data1, forKey: key2) 299 | 300 | // Then 301 | XCTAssertNil(InMemoryStorage.container[nil]?[key1]) 302 | XCTAssertEqual(InMemoryStorage.container[nil]?[key2], data1) 303 | 304 | // When 305 | standardStorage.set(nil, forKey: key2) 306 | 307 | // Then 308 | XCTAssertNil(InMemoryStorage.container[nil]?[key1]) 309 | XCTAssertNil(InMemoryStorage.container[nil]?[key2]) 310 | } 311 | 312 | @InMemoryActor 313 | func testInMemorySetDifferentDomains() { 314 | // Given 315 | let data1 = Data([0xAA, 0xBB, 0xCC]) 316 | let data2 = Data([0xDD, 0xEE, 0xFF]) 317 | let key = "key" 318 | InMemoryStorage.container = [nil: [key: data1], "other": [key: data2]] 319 | 320 | // When 321 | standardStorage.set(data2, forKey: key) 322 | 323 | // Then 324 | XCTAssertEqual(InMemoryStorage.container[nil]?[key], data2) 325 | XCTAssertEqual(InMemoryStorage.container["other"]?[key], data2) 326 | 327 | // When 328 | standardStorage.set(nil, forKey: key) 329 | 330 | // Then 331 | XCTAssertNil(InMemoryStorage.container[nil]?[key]) 332 | XCTAssertEqual(InMemoryStorage.container["other"]?[key], data2) 333 | 334 | // When 335 | otherStorage.set(data1, forKey: key) 336 | 337 | // Then 338 | XCTAssertNil(InMemoryStorage.container[nil]?[key]) 339 | XCTAssertEqual(InMemoryStorage.container["other"]?[key], data1) 340 | 341 | // When 342 | otherStorage.set(nil, forKey: key) 343 | 344 | // Then 345 | XCTAssertNil(InMemoryStorage.container[nil]?[key]) 346 | XCTAssertNil(InMemoryStorage.container["other"]?[key]) 347 | } 348 | 349 | @InMemoryActor 350 | func testInMemoryClear() { 351 | // Given 352 | let data1 = Data([0xAA, 0xBB, 0xCC]) 353 | let data2 = Data([0xDD, 0xEE, 0xFF]) 354 | let key1 = "key1" 355 | let key2 = "key2" 356 | InMemoryStorage.container = [nil: [key1: data1, key2: data2], "other": [key1: data2, key2: data1]] 357 | 358 | // When 359 | standardStorage.clear() 360 | 361 | // Then 362 | XCTAssertEqual(InMemoryStorage.container[nil], [:]) 363 | XCTAssertEqual(InMemoryStorage.container["other"]?[key1], data2) 364 | XCTAssertEqual(InMemoryStorage.container["other"]?[key2], data1) 365 | 366 | // When 367 | standardStorage.clear() 368 | 369 | // Then 370 | XCTAssertEqual(InMemoryStorage.container[nil], [:]) 371 | XCTAssertEqual(InMemoryStorage.container["other"]?[key1], data2) 372 | XCTAssertEqual(InMemoryStorage.container["other"]?[key2], data1) 373 | 374 | // When 375 | InMemoryStorage.container = [nil: [key1: data1, key2: data2], "other": [key1: data2, key2: data1]] 376 | otherStorage.clear() 377 | 378 | // Then 379 | XCTAssertEqual(InMemoryStorage.container[nil]?[key1], data1) 380 | XCTAssertEqual(InMemoryStorage.container[nil]?[key2], data2) 381 | XCTAssertEqual(InMemoryStorage.container["other"], [:]) 382 | 383 | 384 | // When 385 | standardStorage.clear() 386 | 387 | // Then 388 | XCTAssertEqual(InMemoryStorage.container[nil], [:]) 389 | XCTAssertEqual(InMemoryStorage.container["other"], [:]) 390 | } 391 | 392 | @InMemoryActor 393 | func testThreadSafety() { 394 | // Given 395 | let iterations = 5000 396 | let promise = expectation(description: "testThreadSafety") 397 | let group = DispatchGroup() 398 | for _ in 1...iterations { group.enter() } 399 | 400 | // When 401 | DispatchQueue.concurrentPerform(iterations: iterations) { number in 402 | let operation = Int.random(in: 0...4) 403 | let key = "\(Int.random(in: 1000...9999))" 404 | 405 | Task.detached { 406 | switch operation { 407 | case 0: 408 | _ = await self.standardStorage.fetch(forKey: key) 409 | case 1: 410 | await self.standardStorage.save(.init(), forKey: key) 411 | case 2: 412 | await self.standardStorage.delete(forKey: key) 413 | case 3: 414 | await self.standardStorage.set(Bool.random() ? nil : .init(), forKey: key) 415 | case 4: 416 | await self.standardStorage.clear() 417 | default: 418 | break 419 | } 420 | 421 | group.leave() 422 | } 423 | } 424 | 425 | group.notify(queue: .main) { 426 | promise.fulfill() 427 | } 428 | 429 | wait(for: [promise], timeout: 5) 430 | } 431 | } 432 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/DataStorageTests/UserDefaultsStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsStorageTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 31.12.23. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import KeyValueStorage 11 | 12 | final class UserDefaultsStorageTests: XCTestCase { 13 | static let otherStorageDomain = "other" 14 | var standardUserDefaults: UserDefaults! 15 | var otherUserDefaults: UserDefaults! 16 | var standardStorage: UserDefaultsStorage! 17 | var otherStorage: UserDefaultsStorage! 18 | 19 | @UserDefaultsActor 20 | override func setUpWithError() throws { 21 | standardUserDefaults = UserDefaults.standard 22 | otherUserDefaults = UserDefaults(suiteName: Self.otherStorageDomain)! 23 | 24 | standardUserDefaults.clearStandardStorage() 25 | otherUserDefaults.removePersistentDomain(forName: Self.otherStorageDomain) 26 | 27 | standardStorage = UserDefaultsStorage() 28 | otherStorage = try UserDefaultsStorage(domain: Self.otherStorageDomain) 29 | } 30 | 31 | @UserDefaultsActor 32 | func testUserDefaultsDomain() { 33 | XCTAssertEqual(standardStorage.domain, nil) 34 | XCTAssertEqual(otherStorage.domain, Self.otherStorageDomain) 35 | } 36 | 37 | @UserDefaultsActor 38 | func testUserDefaultsFetch() { 39 | // Given 40 | let data1 = Data([0xAA, 0xBB, 0xCC]) 41 | let data2 = Data([0xDD, 0xEE, 0xFF]) 42 | let key1 = "key1" 43 | let key2 = "key2" 44 | 45 | // When 46 | var fetched1 = standardStorage.fetch(forKey: key1) 47 | var fetched2 = standardStorage.fetch(forKey: key2) 48 | 49 | // Then 50 | XCTAssertNil(fetched1) 51 | XCTAssertNil(fetched2) 52 | 53 | // When 54 | standardUserDefaults.set(data1, forKey: key1) 55 | fetched1 = standardStorage.fetch(forKey: key1) 56 | fetched2 = standardStorage.fetch(forKey: key2) 57 | 58 | // Then 59 | XCTAssertEqual(fetched1, data1) 60 | XCTAssertNil(fetched2) 61 | 62 | // When 63 | standardUserDefaults.set(data2, forKey: key1) 64 | fetched1 = standardStorage.fetch(forKey: key1) 65 | fetched2 = standardStorage.fetch(forKey: key2) 66 | 67 | // Then 68 | XCTAssertEqual(fetched1, data2) 69 | XCTAssertNil(fetched2) 70 | 71 | // When 72 | standardUserDefaults.set(data1, forKey: key1) 73 | standardUserDefaults.set(data2, forKey: key2) 74 | fetched1 = standardStorage.fetch(forKey: key1) 75 | fetched2 = standardStorage.fetch(forKey: key2) 76 | 77 | // Then 78 | XCTAssertEqual(fetched1, data1) 79 | XCTAssertEqual(fetched2, data2) 80 | 81 | // When 82 | standardUserDefaults.set(data1, forKey: key2) 83 | standardUserDefaults.set(nil, forKey: key1) 84 | fetched1 = standardStorage.fetch(forKey: key1) 85 | fetched2 = standardStorage.fetch(forKey: key2) 86 | 87 | // Then 88 | XCTAssertNil(fetched1) 89 | XCTAssertEqual(fetched2, data1) 90 | 91 | // When 92 | standardUserDefaults.clearStandardStorage() 93 | fetched1 = standardStorage.fetch(forKey: key1) 94 | fetched2 = standardStorage.fetch(forKey: key2) 95 | 96 | // Then 97 | XCTAssertNil(fetched1) 98 | XCTAssertNil(fetched2) 99 | } 100 | 101 | @UserDefaultsActor 102 | func testUserDefaultsFetchDifferentDomains() { 103 | // Given 104 | let data1 = Data([0xAA, 0xBB, 0xCC]) 105 | let data2 = Data([0xDD, 0xEE, 0xFF]) 106 | let key = "key" 107 | 108 | // When 109 | var fetched1 = standardStorage.fetch(forKey: key) 110 | var fetched2 = otherStorage.fetch(forKey: key) 111 | 112 | // Then 113 | XCTAssertNil(fetched1) 114 | XCTAssertNil(fetched2) 115 | 116 | // When 117 | standardUserDefaults.set(data1, forKey: key) 118 | fetched1 = standardStorage.fetch(forKey: key) 119 | fetched2 = otherStorage.fetch(forKey: key) 120 | 121 | // Then 122 | XCTAssertEqual(fetched1, data1) 123 | XCTAssertNil(fetched2) 124 | 125 | // When 126 | standardUserDefaults.removeObject(forKey: key) 127 | otherUserDefaults.set(data2, forKey: key) 128 | fetched1 = standardStorage.fetch(forKey: key) 129 | fetched2 = otherStorage.fetch(forKey: key) 130 | 131 | // Then 132 | XCTAssertNil(fetched1) 133 | XCTAssertEqual(fetched2, data2) 134 | 135 | // When 136 | standardUserDefaults.set(data2, forKey: key) 137 | otherUserDefaults.removeObject(forKey: key) 138 | fetched1 = standardStorage.fetch(forKey: key) 139 | fetched2 = otherStorage.fetch(forKey: key) 140 | 141 | // Then 142 | XCTAssertEqual(fetched1, data2) 143 | XCTAssertNil(fetched2) 144 | 145 | // When 146 | standardUserDefaults.set(data1, forKey: key) 147 | otherUserDefaults.set(data2, forKey: key) 148 | fetched1 = standardStorage.fetch(forKey: key) 149 | fetched2 = otherStorage.fetch(forKey: key) 150 | 151 | // Then 152 | XCTAssertEqual(fetched1, data1) 153 | XCTAssertEqual(fetched2, data2) 154 | 155 | // When 156 | standardUserDefaults.removeObject(forKey: key) 157 | otherUserDefaults.removeObject(forKey: key) 158 | fetched1 = standardStorage.fetch(forKey: key) 159 | fetched2 = otherStorage.fetch(forKey: key) 160 | 161 | // Then 162 | XCTAssertNil(fetched1) 163 | XCTAssertNil(fetched2) 164 | } 165 | 166 | @UserDefaultsActor 167 | func testUserDefaultsSave() { 168 | // Given 169 | let data1 = Data([0xAA, 0xBB, 0xCC]) 170 | let data2 = Data([0xDD, 0xEE, 0xFF]) 171 | let key1 = "key1" 172 | let key2 = "key2" 173 | 174 | // When 175 | standardStorage.save(data1, forKey: key1) 176 | 177 | // Then 178 | XCTAssertEqual(standardUserDefaults.data(forKey: key1), data1) 179 | XCTAssertNil(standardUserDefaults.data(forKey: key2)) 180 | 181 | // When 182 | standardStorage.save(data2, forKey: key1) 183 | 184 | // Then 185 | XCTAssertEqual(standardUserDefaults.data(forKey: key1), data2) 186 | XCTAssertNil(standardUserDefaults.data(forKey: key2)) 187 | 188 | // When 189 | standardStorage.save(data1, forKey: key2) 190 | 191 | // Then 192 | XCTAssertEqual(standardUserDefaults.data(forKey: key1), data2) 193 | XCTAssertEqual(standardUserDefaults.data(forKey: key2), data1) 194 | } 195 | 196 | @UserDefaultsActor 197 | func testUserDefaultsSaveDifferentDomains() { 198 | // Given 199 | let data1 = Data([0xAA, 0xBB, 0xCC]) 200 | let data2 = Data([0xDD, 0xEE, 0xFF]) 201 | let key = "key" 202 | 203 | // When 204 | standardStorage.save(data1, forKey: key) 205 | 206 | // Then 207 | XCTAssertEqual(standardUserDefaults.data(forKey: key), data1) 208 | XCTAssertNil(otherUserDefaults.data(forKey: key)) 209 | 210 | // When 211 | otherStorage.save(data2, forKey: key) 212 | 213 | // Then 214 | XCTAssertEqual(standardUserDefaults.data(forKey: key), data1) 215 | XCTAssertEqual(otherUserDefaults.data(forKey: key), data2) 216 | 217 | // When 218 | standardStorage.save(data2, forKey: key) 219 | 220 | // Then 221 | XCTAssertEqual(standardUserDefaults.data(forKey: key), data2) 222 | XCTAssertEqual(otherUserDefaults.data(forKey: key), data2) 223 | } 224 | 225 | @UserDefaultsActor 226 | func testUserDefaultsDelete() { 227 | // Given 228 | let data1 = Data([0xAA, 0xBB, 0xCC]) 229 | let data2 = Data([0xDD, 0xEE, 0xFF]) 230 | let key1 = "key1" 231 | let key2 = "key2" 232 | standardUserDefaults.set(data1, forKey: key1) 233 | standardUserDefaults.set(data2, forKey: key2) 234 | 235 | // When 236 | standardStorage.delete(forKey: key1) 237 | 238 | // Then 239 | XCTAssertNil(standardUserDefaults.data(forKey: key1)) 240 | XCTAssertEqual(standardUserDefaults.data(forKey: key2), data2) 241 | 242 | // When 243 | standardStorage.delete(forKey: key1) 244 | 245 | // Then 246 | XCTAssertNil(standardUserDefaults.data(forKey: key1)) 247 | XCTAssertEqual(standardUserDefaults.data(forKey: key2), data2) 248 | 249 | // When 250 | standardStorage.delete(forKey: key2) 251 | 252 | // Then 253 | XCTAssertNil(standardUserDefaults.data(forKey: key1)) 254 | XCTAssertNil(standardUserDefaults.data(forKey: key2)) 255 | } 256 | 257 | @UserDefaultsActor 258 | func testUserDefaultsDeleteDifferentDomains() { 259 | // Given 260 | let data1 = Data([0xAA, 0xBB, 0xCC]) 261 | let data2 = Data([0xDD, 0xEE, 0xFF]) 262 | let key = "key" 263 | standardUserDefaults.set(data1, forKey: key) 264 | otherUserDefaults.set(data2, forKey: key) 265 | 266 | // When 267 | standardStorage.delete(forKey: key) 268 | 269 | // Then 270 | XCTAssertNil(standardUserDefaults.data(forKey: key)) 271 | XCTAssertEqual(otherUserDefaults.data(forKey: key), data2) 272 | 273 | // When 274 | standardStorage.delete(forKey: key) 275 | 276 | // Then 277 | XCTAssertNil(standardUserDefaults.data(forKey: key)) 278 | XCTAssertEqual(otherUserDefaults.data(forKey: key), data2) 279 | 280 | // When 281 | otherStorage.delete(forKey: key) 282 | 283 | // Then 284 | XCTAssertNil(standardUserDefaults.data(forKey: key)) 285 | XCTAssertNil(otherUserDefaults.data(forKey: key)) 286 | } 287 | 288 | @UserDefaultsActor 289 | func testUserDefaultsSet() { 290 | // Given 291 | let data1 = Data([0xAA, 0xBB, 0xCC]) 292 | let data2 = Data([0xDD, 0xEE, 0xFF]) 293 | let key1 = "key1" 294 | let key2 = "key2" 295 | standardUserDefaults.set(data1, forKey: key1) 296 | standardUserDefaults.set(data2, forKey: key2) 297 | 298 | // When 299 | standardStorage.set(data2, forKey: key1) 300 | 301 | // Then 302 | XCTAssertEqual(standardUserDefaults.data(forKey: key1), data2) 303 | XCTAssertEqual(standardUserDefaults.data(forKey: key2), data2) 304 | 305 | // When 306 | standardStorage.set(nil, forKey: key1) 307 | 308 | // Then 309 | XCTAssertNil(standardUserDefaults.data(forKey: key1)) 310 | XCTAssertEqual(standardUserDefaults.data(forKey: key2), data2) 311 | 312 | // When 313 | standardStorage.set(data1, forKey: key2) 314 | 315 | // Then 316 | XCTAssertNil(standardUserDefaults.data(forKey: key1)) 317 | XCTAssertEqual(standardUserDefaults.data(forKey: key2), data1) 318 | 319 | // When 320 | standardStorage.set(nil, forKey: key2) 321 | 322 | // Then 323 | XCTAssertNil(standardUserDefaults.data(forKey: key1)) 324 | XCTAssertNil(standardUserDefaults.data(forKey: key2)) 325 | } 326 | 327 | @UserDefaultsActor 328 | func testUserDefaultsSetDifferentDomains() { 329 | // Given 330 | let data1 = Data([0xAA, 0xBB, 0xCC]) 331 | let data2 = Data([0xDD, 0xEE, 0xFF]) 332 | let key = "key" 333 | standardUserDefaults.set(data1, forKey: key) 334 | otherUserDefaults.set(data2, forKey: key) 335 | 336 | // When 337 | standardStorage.set(data2, forKey: key) 338 | 339 | // Then 340 | XCTAssertEqual(standardUserDefaults.data(forKey: key), data2) 341 | XCTAssertEqual(otherUserDefaults.data(forKey: key), data2) 342 | 343 | // When 344 | standardStorage.set(nil, forKey: key) 345 | 346 | // Then 347 | XCTAssertNil(standardUserDefaults.data(forKey: key)) 348 | XCTAssertEqual(otherUserDefaults.data(forKey: key), data2) 349 | 350 | // When 351 | otherStorage.set(data1, forKey: key) 352 | 353 | // Then 354 | XCTAssertNil(standardUserDefaults.data(forKey: key)) 355 | XCTAssertEqual(otherUserDefaults.data(forKey: key), data1) 356 | 357 | // When 358 | otherStorage.set(nil, forKey: key) 359 | 360 | // Then 361 | XCTAssertNil(standardUserDefaults.data(forKey: key)) 362 | XCTAssertNil(otherUserDefaults.data(forKey: key)) 363 | } 364 | 365 | @UserDefaultsActor 366 | func testUserDefaultsClear() { 367 | // Given 368 | let data1 = Data([0xAA, 0xBB, 0xCC]) 369 | let data2 = Data([0xDD, 0xEE, 0xFF]) 370 | let key1 = "key1" 371 | let key2 = "key2" 372 | standardUserDefaults.set(data1, forKey: key1) 373 | standardUserDefaults.set(data2, forKey: key2) 374 | otherUserDefaults.set(data1, forKey: key1) 375 | otherUserDefaults.set(data2, forKey: key2) 376 | 377 | // When 378 | standardStorage.clear() 379 | 380 | // Then 381 | XCTAssertNil(standardUserDefaults.data(forKey: key1)) 382 | XCTAssertNil(standardUserDefaults.data(forKey: key2)) 383 | XCTAssertEqual(otherUserDefaults.data(forKey: key1), data1) 384 | XCTAssertEqual(otherUserDefaults.data(forKey: key2), data2) 385 | 386 | // When 387 | standardStorage.clear() 388 | 389 | // Then 390 | XCTAssertNil(standardUserDefaults.data(forKey: key1)) 391 | XCTAssertNil(standardUserDefaults.data(forKey: key2)) 392 | XCTAssertEqual(otherUserDefaults.data(forKey: key1), data1) 393 | XCTAssertEqual(otherUserDefaults.data(forKey: key2), data2) 394 | 395 | // When 396 | standardUserDefaults.set(data2, forKey: key1) 397 | standardUserDefaults.set(data1, forKey: key2) 398 | otherStorage.clear() 399 | 400 | // Then 401 | XCTAssertEqual(standardUserDefaults.data(forKey: key1), data2) 402 | XCTAssertEqual(standardUserDefaults.data(forKey: key2), data1) 403 | XCTAssertNil(otherUserDefaults.data(forKey: key1)) 404 | XCTAssertNil(otherUserDefaults.data(forKey: key2)) 405 | 406 | // When 407 | standardStorage.clear() 408 | 409 | // Then 410 | XCTAssertNil(standardUserDefaults.data(forKey: key1)) 411 | XCTAssertNil(standardUserDefaults.data(forKey: key2)) 412 | XCTAssertNil(otherUserDefaults.data(forKey: key1)) 413 | XCTAssertNil(otherUserDefaults.data(forKey: key2)) 414 | } 415 | 416 | @UserDefaultsActor 417 | func testInitCustomUserDefaults() { 418 | // Given 419 | let userDefaults = UserDefaults(suiteName: "mock")! 420 | 421 | // When 422 | let storage = UserDefaultsStorage(userDefaults: userDefaults) 423 | 424 | // Then 425 | XCTAssertNil(storage.domain) 426 | } 427 | 428 | @UserDefaultsActor 429 | func testThreadSafety() { 430 | // Given 431 | let iterations = 5000 432 | let promise = expectation(description: "testThreadSafety") 433 | let group = DispatchGroup() 434 | for _ in 1...iterations { group.enter() } 435 | 436 | // When 437 | DispatchQueue.concurrentPerform(iterations: iterations) { number in 438 | let operation = Int.random(in: 0...4) 439 | let key = "\(Int.random(in: 1000...9999))" 440 | 441 | Task.detached { 442 | switch operation { 443 | case 0: 444 | _ = await self.standardStorage.fetch(forKey: key) 445 | case 1: 446 | await self.standardStorage.save(.init(), forKey: key) 447 | case 2: 448 | await self.standardStorage.delete(forKey: key) 449 | case 3: 450 | await self.standardStorage.set(Bool.random() ? nil : .init(), forKey: key) 451 | case 4: 452 | await self.standardStorage.clear() 453 | default: 454 | break 455 | } 456 | 457 | group.leave() 458 | } 459 | } 460 | 461 | group.notify(queue: .main) { 462 | promise.fulfill() 463 | } 464 | 465 | wait(for: [promise], timeout: 5) 466 | } 467 | } 468 | 469 | extension UserDefaults { 470 | func clearStandardStorage() { 471 | self.removePersistentDomain(forName: Bundle.main.bundleIdentifier!) 472 | } 473 | } 474 | 475 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageLegacyTests/KeyValueStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyValueStorageTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 7/27/22. 6 | // 7 | 8 | import XCTest 9 | @testable import KeyValueStorageLegacy 10 | 11 | #if os(macOS) 12 | final class KeyValueStorageTests: XCTestCase { 13 | private var storage: KeyValueStorage! 14 | 15 | override func setUp() { 16 | storage = KeyValueStorage() 17 | } 18 | 19 | override func tearDown() { 20 | storage.clear() 21 | } 22 | 23 | // MARK: - Test native types 24 | 25 | func testInt() { 26 | // Given 27 | let integer = 17 28 | let key = KeyValueStorageKey(name: "anInteger") 29 | 30 | // When 31 | storage.save(integer, forKey: key) 32 | // Then 33 | XCTAssertEqual(integer, storage.fetch(forKey: key)) 34 | 35 | // When 36 | storage.delete(forKey: key) 37 | // Then 38 | XCTAssertNil(storage.fetch(forKey: key)) 39 | } 40 | 41 | func testString() { 42 | // Given 43 | let string = "someString" 44 | let key = KeyValueStorageKey(name: "aString") 45 | 46 | // When 47 | storage.save(string, forKey: key) 48 | // Then 49 | XCTAssertEqual(string, storage.fetch(forKey: key)) 50 | 51 | // When 52 | storage.delete(forKey: key) 53 | // Then 54 | XCTAssertNil(storage.fetch(forKey: key)) 55 | } 56 | 57 | func testDate() { 58 | // Given 59 | let date = Date() 60 | let key = KeyValueStorageKey(name: "aDate") 61 | 62 | // When 63 | storage.save(date, forKey: key) 64 | // Then 65 | XCTAssertEqual(date, storage.fetch(forKey: key)) 66 | 67 | // When 68 | storage.delete(forKey: key) 69 | // Then 70 | XCTAssertNil(storage.fetch(forKey: key)) 71 | } 72 | 73 | func testArray() { 74 | // Given 75 | let array = [1, 2, 3, 4] 76 | let key = KeyValueStorageKey<[Int]>(name: "anArray") 77 | 78 | // When 79 | storage.save(array, forKey: key) 80 | // Then 81 | XCTAssertEqual(array, storage.fetch(forKey: key)) 82 | 83 | // When 84 | storage.delete(forKey: key) 85 | // Then 86 | XCTAssertNil(storage.fetch(forKey: key)) 87 | } 88 | 89 | func testDictionary1() { 90 | // Given 91 | let dictionary: [String: Int] = ["a": 1, "b": 2] 92 | let key = KeyValueStorageKey<[String: Int]>(name: "aDictionary") 93 | 94 | // When 95 | storage.save(dictionary, forKey: key) 96 | // Then 97 | XCTAssertEqual(dictionary, storage.fetch(forKey: key)) 98 | 99 | // When 100 | storage.delete(forKey: key) 101 | // Then 102 | XCTAssertNil(storage.fetch(forKey: key)) 103 | } 104 | 105 | func testDictionary2() { 106 | // Given 107 | let dictionary: [Int: String] = [1: "a", 2: "b"] 108 | let key = KeyValueStorageKey<[Int: String]>(name: "aDictionary") 109 | 110 | // When 111 | storage.save(dictionary, forKey: key) 112 | // Then 113 | XCTAssertEqual(dictionary, storage.fetch(forKey: key)) 114 | 115 | // When 116 | storage.delete(forKey: key) 117 | // Then 118 | XCTAssertNil(storage.fetch(forKey: key)) 119 | } 120 | 121 | func testIntInMemoryDefault() { 122 | // Given 123 | let integer = 17 124 | let key = KeyValueStorageKey(name: "anInteger", storage: .inMemory) 125 | let otherStorage = KeyValueStorage() 126 | 127 | // When 128 | storage.save(integer, forKey: key) 129 | // Then 130 | XCTAssertEqual(integer, storage.fetch(forKey: key)) 131 | XCTAssertEqual(integer, otherStorage.fetch(forKey: key)) 132 | 133 | // When 134 | otherStorage.delete(forKey: key) 135 | // Then 136 | XCTAssertNil(storage.fetch(forKey: key)) 137 | } 138 | 139 | func testIntInMemoryDifferentWithGroup() { 140 | // Given 141 | let integer = 17 142 | let key = KeyValueStorageKey(name: "anInteger", storage: .inMemory) 143 | let otherStorage = KeyValueStorage(accessGroup: UUID().uuidString, teamID: "xxx") 144 | 145 | // When 146 | storage.save(integer, forKey: key) 147 | // Then 148 | XCTAssertEqual(integer, storage.fetch(forKey: key)) 149 | XCTAssertNil(otherStorage.fetch(forKey: key)) 150 | 151 | // When 152 | otherStorage.delete(forKey: key) 153 | // Then 154 | XCTAssertEqual(integer, storage.fetch(forKey: key)) 155 | } 156 | 157 | func testIntInMemorySameWithGroup() { 158 | // Given 159 | let integer = 17 160 | let key = KeyValueStorageKey(name: "anInteger", storage: .inMemory) 161 | let otherStorage = KeyValueStorage(accessGroup: "accessGroup", teamID: "teamID") 162 | storage = KeyValueStorage(accessGroup: "accessGroup", teamID: "teamID") 163 | 164 | // When 165 | storage.save(integer, forKey: key) 166 | // Then 167 | XCTAssertEqual(integer, storage.fetch(forKey: key)) 168 | XCTAssertEqual(integer, otherStorage.fetch(forKey: key)) 169 | 170 | // When 171 | otherStorage.delete(forKey: key) 172 | // Then 173 | XCTAssertNil(storage.fetch(forKey: key)) 174 | } 175 | 176 | func testIntSecure() { 177 | // Given 178 | let integer = 17 179 | let key = KeyValueStorageKey(name: "anInteger", storage: .keychain()) 180 | 181 | // When 182 | storage.save(integer, forKey: key) 183 | // Then 184 | XCTAssertEqual(integer, storage.fetch(forKey: key)) 185 | 186 | // When 187 | storage.delete(forKey: key) 188 | // Then 189 | XCTAssertNil(storage.fetch(forKey: key)) 190 | } 191 | 192 | func testStringSecure() { 193 | // Given 194 | let string = "someString" 195 | let key = KeyValueStorageKey(name: "aString", storage: .keychain()) 196 | 197 | // When 198 | storage.save(string, forKey: key) 199 | // Then 200 | XCTAssertEqual(string, storage.fetch(forKey: key)) 201 | 202 | // When 203 | storage.delete(forKey: key) 204 | // Then 205 | XCTAssertNil(storage.fetch(forKey: key)) 206 | } 207 | 208 | func testDateSecure() { 209 | // Given 210 | let date = Date() 211 | let key = KeyValueStorageKey(name: "aDate", storage: .keychain()) 212 | 213 | storage.save(date, forKey: key) 214 | // Then 215 | XCTAssertEqual(date, storage.fetch(forKey: key)) 216 | 217 | storage.delete(forKey: key) 218 | // Then 219 | XCTAssertNil(storage.fetch(forKey: key)) 220 | } 221 | 222 | func testArraySecure() { 223 | // Given 224 | let array = [1, 2, 3, 4] 225 | let key = KeyValueStorageKey<[Int]>(name: "anArray", storage: .keychain()) 226 | 227 | // When 228 | storage.save(array, forKey: key) 229 | // Then 230 | XCTAssertEqual(array, storage.fetch(forKey: key)) 231 | 232 | // When 233 | storage.delete(forKey: key) 234 | // Then 235 | XCTAssertNil(storage.fetch(forKey: key)) 236 | } 237 | 238 | func testDictionary1Secure() { 239 | // Given 240 | let dictionary: [String: Int] = ["a": 1, "b": 2] 241 | let key = KeyValueStorageKey<[String: Int]>(name: "aDictionary", storage: .keychain()) 242 | 243 | // When 244 | storage.save(dictionary, forKey: key) 245 | // Then 246 | XCTAssertEqual(dictionary, storage.fetch(forKey: key)) 247 | 248 | // When 249 | storage.delete(forKey: key) 250 | // Then 251 | XCTAssertNil(storage.fetch(forKey: key)) 252 | } 253 | 254 | func testDictionary2Secure() { 255 | // Given 256 | let dictionary: [Int: String] = [1: "a", 2: "b"] 257 | let key = KeyValueStorageKey<[Int: String]>(name: "aDictionary", storage: .keychain()) 258 | 259 | // When 260 | storage.save(dictionary, forKey: key) 261 | // Then 262 | XCTAssertEqual(dictionary, storage.fetch(forKey: key)) 263 | 264 | // When 265 | storage.delete(forKey: key) 266 | // Then 267 | XCTAssertNil(storage.fetch(forKey: key)) 268 | } 269 | 270 | // MARK: - Test custom types 271 | 272 | func testStruct() { 273 | // Given 274 | let structure = SomeStruct(string: "struct", integer: 8, date: Date(timeIntervalSince1970: 44651)) 275 | let key = KeyValueStorageKey(name: "aStruct") 276 | 277 | // When 278 | storage.save(structure, forKey: key) 279 | // Then 280 | XCTAssertEqual(structure, storage.fetch(forKey: key)) 281 | 282 | storage.delete(forKey: key) 283 | // Then 284 | XCTAssertNil(storage.fetch(forKey: key)) 285 | } 286 | 287 | func testClass() { 288 | // Given 289 | let classification = SomeClass(double: 5.67, array: [8, 5], dict: ["some": "thing"]) 290 | let key = KeyValueStorageKey(name: "aClass") 291 | 292 | // When 293 | storage.save(classification, forKey: key) 294 | // Then 295 | XCTAssertEqual(classification, storage.fetch(forKey: key)) 296 | XCTAssertFalse(classification === storage.fetch(forKey: key)) 297 | // When 298 | storage.delete(forKey: key) 299 | // Then 300 | XCTAssertNil(storage.fetch(forKey: key)) 301 | } 302 | 303 | func testStructSecure() { 304 | // Given 305 | let structure = SomeStruct(string: "struct", integer: 8, date: Date(timeIntervalSince1970: 44651)) 306 | let key = KeyValueStorageKey(name: "aStruct", storage: .keychain()) 307 | 308 | // When 309 | storage.save(structure, forKey: key) 310 | // Then 311 | XCTAssertEqual(structure, storage.fetch(forKey: key)) 312 | 313 | // When 314 | storage.delete(forKey: key) 315 | // Then 316 | XCTAssertNil(storage.fetch(forKey: key)) 317 | } 318 | 319 | func testClassSecure() { 320 | // Given 321 | let classification = SomeClass(double: 5.67, array: [8, 5], dict: ["some": "thing"]) 322 | let key = KeyValueStorageKey(name: "aClass", storage: .keychain()) 323 | 324 | // When 325 | storage.save(classification, forKey: key) 326 | // Then 327 | XCTAssertEqual(classification, storage.fetch(forKey: key)) 328 | XCTAssertFalse(classification === storage.fetch(forKey: key)) 329 | // When 330 | storage.delete(forKey: key) 331 | // Then 332 | XCTAssertNil(storage.fetch(forKey: key)) 333 | } 334 | 335 | // MARK: - Testing edge cases 336 | 337 | func testWrongFetchType() { 338 | // Given 339 | let classification = SomeClass(double: 5.67, array: [8, 5], dict: ["some": "thing"]) 340 | let key = KeyValueStorageKey(name: "aClass") 341 | let wrongKey = KeyValueStorageKey(name: "aClass") 342 | 343 | // When 344 | storage.save(classification, forKey: key) 345 | // Then 346 | XCTAssertEqual(classification, storage.fetch(forKey: key)) 347 | XCTAssertNil(storage.fetch(forKey: wrongKey)) 348 | 349 | // When 350 | storage.delete(forKey: key) 351 | // Then 352 | XCTAssertNil(storage.fetch(forKey: key)) 353 | } 354 | 355 | func testWrongFetchSameKeyName() { 356 | // Given 357 | let classification = SomeClass(double: 5.67, array: [8, 5], dict: ["some": "thing"]) 358 | let key = KeyValueStorageKey(name: "aClass") 359 | let wrongKey = KeyValueStorageKey(name: "aClass", storage: .keychain()) 360 | 361 | // When 362 | storage.save(classification, forKey: key) 363 | // Then 364 | XCTAssertEqual(classification, storage.fetch(forKey: key)) 365 | XCTAssertNil(storage.fetch(forKey: wrongKey)) 366 | 367 | // When 368 | storage.delete(forKey: key) 369 | // Then 370 | XCTAssertNil(storage.fetch(forKey: key)) 371 | } 372 | 373 | func testWrongSave() { 374 | // Given 375 | let classification = SomeClass(double: .infinity, array: [8, 5], dict: ["some": "thing"]) 376 | let key1 = KeyValueStorageKey(name: "aClass") 377 | let key2 = KeyValueStorageKey(name: "aClass", storage: .keychain()) 378 | let key3 = KeyValueStorageKey(name: "aClass", storage: .inMemory) 379 | 380 | // When 381 | storage.save(classification, forKey: key1) 382 | // Then 383 | XCTAssertNil(storage.fetch(forKey: key1)) 384 | 385 | // When 386 | storage.save(classification, forKey: key2) 387 | // Then 388 | XCTAssertNil(storage.fetch(forKey: key2)) 389 | 390 | // When 391 | storage.save(classification, forKey: key3) 392 | // Then 393 | XCTAssertEqual(classification, storage.fetch(forKey: key3)) 394 | } 395 | 396 | func testSaveSameKeyName() { 397 | // Given 398 | let class1 = SomeClass(double: 5.67, array: [8, 5], dict: ["some": "thing"]) 399 | let class2 = SomeClass(double: 6.67, array: [5, 8], dict: ["thing": "some"]) 400 | let class3 = SomeClass(double: 8.88, array: [4, 3], dict: ["another": "stuff"]) 401 | let class4 = SomeClass(double: 0, array: [0, 0], dict: ["zero": "empty"]) 402 | let key1 = KeyValueStorageKey(name: "aClass") 403 | let key2 = KeyValueStorageKey(name: "aClass", storage: .keychain()) 404 | let key3 = KeyValueStorageKey(name: "aClass", storage: .inMemory) 405 | 406 | // When 407 | storage.save(class1, forKey: key1) 408 | // Then 409 | XCTAssertEqual(class1, storage.fetch(forKey: key1)) 410 | 411 | // When 412 | storage.save(class2, forKey: key2) 413 | // Then 414 | XCTAssertEqual(class2, storage.fetch(forKey: key2)) 415 | 416 | // When 417 | storage.save(class2, forKey: key2) 418 | // Then 419 | XCTAssertEqual(class2, storage.fetch(forKey: key2)) 420 | 421 | // When 422 | storage.save(class3, forKey: key3) 423 | // Then 424 | XCTAssertEqual(class3, storage.fetch(forKey: key3)) 425 | 426 | // When 427 | storage.save(class4, forKey: key1) 428 | // Then 429 | XCTAssertEqual(class4, storage.fetch(forKey: key1)) 430 | XCTAssertNotEqual(class4, storage.fetch(forKey: key2)) 431 | XCTAssertNotEqual(class4, storage.fetch(forKey: key3)) 432 | 433 | // When 434 | storage.save(class4, forKey: key2) 435 | // Then 436 | XCTAssertEqual(class4, storage.fetch(forKey: key1)) 437 | XCTAssertEqual(class4, storage.fetch(forKey: key2)) 438 | XCTAssertNotEqual(class4, storage.fetch(forKey: key3)) 439 | 440 | // When 441 | storage.save(class4, forKey: key3) 442 | // Then 443 | XCTAssertEqual(class4, storage.fetch(forKey: key1)) 444 | XCTAssertEqual(class4, storage.fetch(forKey: key2)) 445 | XCTAssertEqual(class4, storage.fetch(forKey: key3)) 446 | } 447 | 448 | func testDeleteSameKeyName() { 449 | // Given 450 | let class1 = SomeClass(double: 5.67, array: [8, 5], dict: ["some": "thing"]) 451 | let class2 = SomeClass(double: 6.67, array: [5, 8], dict: ["thing": "some"]) 452 | let class3 = SomeClass(double: 8.88, array: [4, 3], dict: ["another": "stuff"]) 453 | let key1 = KeyValueStorageKey(name: "aClass") 454 | let key2 = KeyValueStorageKey(name: "aClass", storage: .keychain()) 455 | let key3 = KeyValueStorageKey(name: "aClass", storage: .inMemory) 456 | 457 | // When 458 | storage.save(class1, forKey: key1) 459 | // Then 460 | XCTAssertEqual(class1, storage.fetch(forKey: key1)) 461 | 462 | // When 463 | storage.save(class2, forKey: key2) 464 | // Then 465 | XCTAssertEqual(class2, storage.fetch(forKey: key2)) 466 | 467 | // When 468 | storage.save(class3, forKey: key3) 469 | // Then 470 | XCTAssertEqual(class3, storage.fetch(forKey: key3)) 471 | 472 | // When 473 | storage.delete(forKey: key1) 474 | // Then 475 | XCTAssertNil(storage.fetch(forKey: key1)) 476 | XCTAssertNotNil(storage.fetch(forKey: key2)) 477 | XCTAssertNotNil(storage.fetch(forKey: key3)) 478 | 479 | // When 480 | storage.delete(forKey: key2) 481 | // Then 482 | XCTAssertNil(storage.fetch(forKey: key1)) 483 | XCTAssertNil(storage.fetch(forKey: key2)) 484 | XCTAssertNotNil(storage.fetch(forKey: key3)) 485 | 486 | // When 487 | storage.delete(forKey: key3) 488 | // Then 489 | XCTAssertNil(storage.fetch(forKey: key1)) 490 | XCTAssertNil(storage.fetch(forKey: key2)) 491 | XCTAssertNil(storage.fetch(forKey: key3)) 492 | } 493 | 494 | func testSet() { 495 | // Given 496 | let string = "someString" 497 | let key = KeyValueStorageKey(name: "aString", storage: .keychain()) 498 | 499 | // When 500 | storage.set(string, forKey: key) 501 | // Then 502 | XCTAssertEqual(string, storage.fetch(forKey: key)) 503 | 504 | // When 505 | storage.set(nil, forKey: key) 506 | // Then 507 | XCTAssertNil(storage.fetch(forKey: key)) 508 | } 509 | 510 | func testClear() { 511 | // Given 512 | let integer1 = 17 513 | let integer2 = 8 514 | let integer3 = 6 515 | let key1 = KeyValueStorageKey(name: "anInteger1", storage: .keychain()) 516 | let key2 = KeyValueStorageKey(name: "anInteger2", storage: .userDefaults) 517 | let key3 = KeyValueStorageKey(name: "anInteger3", storage: .inMemory) 518 | 519 | storage.save(integer1, forKey: key1) 520 | storage.save(integer2, forKey: key2) 521 | storage.save(integer3, forKey: key3) 522 | 523 | XCTAssertEqual(integer1, storage.fetch(forKey: key1)) 524 | XCTAssertEqual(integer2, storage.fetch(forKey: key2)) 525 | XCTAssertEqual(integer3, storage.fetch(forKey: key3)) 526 | 527 | // When 528 | storage.clear() 529 | 530 | // Then 531 | XCTAssertNil(storage.fetch(forKey: key1)) 532 | XCTAssertNil(storage.fetch(forKey: key2)) 533 | XCTAssertNil(storage.fetch(forKey: key3)) 534 | } 535 | 536 | func testAccessGroup() { 537 | // Given 538 | let accessGroup = "group" 539 | XCTAssertNil(storage.accessGroup) 540 | 541 | // When 542 | storage = KeyValueStorage(accessGroup: accessGroup, teamID: "team") 543 | 544 | // Then 545 | XCTAssertEqual(accessGroup, storage.accessGroup) 546 | } 547 | } 548 | 549 | struct SomeStruct: Equatable, Codable { 550 | var string: String 551 | var integer: Int 552 | var date: Date 553 | } 554 | 555 | class SomeClass: Equatable, Codable { 556 | var double: Double 557 | var array: [Int] 558 | var dict: [String: String] 559 | 560 | init(double: Double, array: [Int], dict: [String: String]) { 561 | self.double = double 562 | self.array = array 563 | self.dict = dict 564 | } 565 | 566 | static func == (lhs: SomeClass, rhs: SomeClass) -> Bool { 567 | lhs.double == rhs.double && lhs.array == rhs.array && lhs.dict == rhs.dict 568 | } 569 | } 570 | #endif 571 | -------------------------------------------------------------------------------- /Tests/KeyValueStorageTests/DataStorageTests/FileStorageTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FileStorageTests.swift 3 | // 4 | // 5 | // Created by Narek Sahakyan on 31.12.23. 6 | // 7 | 8 | import XCTest 9 | import Foundation 10 | @testable import KeyValueStorage 11 | 12 | final class FileStorageTests: XCTestCase { 13 | static let otherStorageDomain = "other" 14 | var fileManager = FileManager.default 15 | var standardPath: URL! 16 | var otherPath: URL! 17 | var standardStorage: FileStorage! 18 | var otherStorage: FileStorage! 19 | 20 | @FileActor 21 | override func setUpWithError() throws { 22 | let id = Bundle.main.bundleIdentifier! 23 | standardPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent(id, isDirectory: true) 24 | otherPath = fileManager.containerURL(forSecurityApplicationGroupIdentifier: Self.otherStorageDomain)!.appendingPathComponent(id, isDirectory: true) 25 | 26 | try? fileManager.createDirectory(at: standardPath, withIntermediateDirectories: true) 27 | try? fileManager.createDirectory(at: otherPath, withIntermediateDirectories: true) 28 | try fileManager.clearDirectoryContents(url: standardPath) 29 | try fileManager.clearDirectoryContents(url: otherPath) 30 | 31 | standardStorage = try FileStorage() 32 | otherStorage = try FileStorage(domain: Self.otherStorageDomain) 33 | } 34 | 35 | @FileActor 36 | func testFileDomain() { 37 | XCTAssertEqual(standardStorage.domain, nil) 38 | XCTAssertEqual(otherStorage.domain, Self.otherStorageDomain) 39 | } 40 | 41 | @FileActor 42 | func testFileFetch() throws { 43 | // Given 44 | let data1 = Data([0xAA, 0xBB, 0xCC]) 45 | let data2 = Data([0xDD, 0xEE, 0xFF]) 46 | let key1 = "key1" 47 | let key2 = "key2" 48 | let filePath1 = standardPath.appendingPathComponent(key1).path 49 | let filePath2 = standardPath.appendingPathComponent(key2).path 50 | 51 | // When 52 | var fetched1 = try standardStorage.fetch(forKey: key1) 53 | var fetched2 = try standardStorage.fetch(forKey: key2) 54 | 55 | // Then 56 | XCTAssertNil(fetched1) 57 | XCTAssertNil(fetched2) 58 | 59 | // When 60 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data1)) 61 | fetched1 = try standardStorage.fetch(forKey: key1) 62 | fetched2 = try standardStorage.fetch(forKey: key2) 63 | 64 | // Then 65 | XCTAssertEqual(fetched1, data1) 66 | XCTAssertNil(fetched2) 67 | 68 | // When 69 | try fileManager.removeItem(atPath: filePath1) 70 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data2)) 71 | fetched1 = try standardStorage.fetch(forKey: key1) 72 | fetched2 = try standardStorage.fetch(forKey: key2) 73 | 74 | // Then 75 | XCTAssertEqual(fetched1, data2) 76 | XCTAssertNil(fetched2) 77 | 78 | // When 79 | try fileManager.removeItem(atPath: filePath1) 80 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data1)) 81 | XCTAssertTrue(fileManager.createFile(atPath: filePath2, contents: data2)) 82 | fetched1 = try standardStorage.fetch(forKey: key1) 83 | fetched2 = try standardStorage.fetch(forKey: key2) 84 | 85 | // Then 86 | XCTAssertEqual(fetched1, data1) 87 | XCTAssertEqual(fetched2, data2) 88 | 89 | // When 90 | try fileManager.removeItem(atPath: filePath1) 91 | try fileManager.removeItem(atPath: filePath2) 92 | XCTAssertTrue(fileManager.createFile(atPath: filePath2, contents: data1)) 93 | fetched1 = try standardStorage.fetch(forKey: key1) 94 | fetched2 = try standardStorage.fetch(forKey: key2) 95 | 96 | // Then 97 | XCTAssertNil(fetched1) 98 | XCTAssertEqual(fetched2, data1) 99 | 100 | // When 101 | try fileManager.removeItem(atPath: filePath2) 102 | fetched1 = try standardStorage.fetch(forKey: key1) 103 | fetched2 = try standardStorage.fetch(forKey: key2) 104 | 105 | // Then 106 | XCTAssertNil(fetched1) 107 | XCTAssertNil(fetched2) 108 | } 109 | 110 | @FileActor 111 | func testFileFetchDifferentDomains() throws { 112 | // Given 113 | let data1 = Data([0xAA, 0xBB, 0xCC]) 114 | let data2 = Data([0xDD, 0xEE, 0xFF]) 115 | let key = "key" 116 | let filePath1 = standardPath.appendingPathComponent(key).path 117 | let filePath2 = otherPath.appendingPathComponent(key).path 118 | 119 | // When 120 | var fetched1 = try standardStorage.fetch(forKey: key) 121 | var fetched2 = try otherStorage.fetch(forKey: key) 122 | 123 | // Then 124 | XCTAssertNil(fetched1) 125 | XCTAssertNil(fetched2) 126 | 127 | // When 128 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data1)) 129 | fetched1 = try standardStorage.fetch(forKey: key) 130 | fetched2 = try otherStorage.fetch(forKey: key) 131 | 132 | // Then 133 | XCTAssertEqual(fetched1, data1) 134 | XCTAssertNil(fetched2) 135 | 136 | // When 137 | try fileManager.removeItem(atPath: filePath1) 138 | XCTAssertTrue(fileManager.createFile(atPath: filePath2, contents: data2)) 139 | fetched1 = try standardStorage.fetch(forKey: key) 140 | fetched2 = try otherStorage.fetch(forKey: key) 141 | 142 | // Then 143 | XCTAssertNil(fetched1) 144 | XCTAssertEqual(fetched2, data2) 145 | 146 | // When 147 | 148 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data2)) 149 | try fileManager.removeItem(atPath: filePath2) 150 | fetched1 = try standardStorage.fetch(forKey: key) 151 | fetched2 = try otherStorage.fetch(forKey: key) 152 | 153 | // Then 154 | XCTAssertEqual(fetched1, data2) 155 | XCTAssertNil(fetched2) 156 | 157 | // When 158 | try fileManager.removeItem(atPath: filePath1) 159 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data1)) 160 | XCTAssertTrue(fileManager.createFile(atPath: filePath2, contents: data2)) 161 | fetched1 = try standardStorage.fetch(forKey: key) 162 | fetched2 = try otherStorage.fetch(forKey: key) 163 | 164 | // Then 165 | XCTAssertEqual(fetched1, data1) 166 | XCTAssertEqual(fetched2, data2) 167 | 168 | // When 169 | try fileManager.removeItem(atPath: filePath1) 170 | try fileManager.removeItem(atPath: filePath2) 171 | fetched1 = try standardStorage.fetch(forKey: key) 172 | fetched2 = try otherStorage.fetch(forKey: key) 173 | 174 | // Then 175 | XCTAssertNil(fetched1) 176 | XCTAssertNil(fetched2) 177 | } 178 | 179 | @FileActor 180 | func testFileSave() throws { 181 | // Given 182 | let data1 = Data([0xAA, 0xBB, 0xCC]) 183 | let data2 = Data([0xDD, 0xEE, 0xFF]) 184 | let key1 = "key1" 185 | let key2 = "key2" 186 | let filePath1 = standardPath.appendingPathComponent(key1).path 187 | let filePath2 = standardPath.appendingPathComponent(key2).path 188 | 189 | // When 190 | try standardStorage.save(data1, forKey: key1) 191 | 192 | // Then 193 | XCTAssertEqual(fileManager.contents(atPath: filePath1), data1) 194 | XCTAssertNil(fileManager.contents(atPath: filePath2)) 195 | 196 | // When 197 | try standardStorage.save(data2, forKey: key1) 198 | try standardStorage.save(data2, forKey: key1) 199 | try standardStorage.save(data2, forKey: key1) 200 | try standardStorage.save(data2, forKey: key1) 201 | 202 | // Then 203 | XCTAssertEqual(fileManager.contents(atPath: filePath1), data2) 204 | XCTAssertNil(fileManager.contents(atPath: filePath2)) 205 | 206 | // When 207 | try standardStorage.save(data1, forKey: key2) 208 | 209 | // Then 210 | XCTAssertEqual(fileManager.contents(atPath: filePath1), data2) 211 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data1) 212 | } 213 | 214 | @FileActor 215 | func testFileSaveDifferentDomains() throws { 216 | // Given 217 | let data1 = Data([0xAA, 0xBB, 0xCC]) 218 | let data2 = Data([0xDD, 0xEE, 0xFF]) 219 | let key = "key" 220 | let filePath1 = standardPath.appendingPathComponent(key).path 221 | let filePath2 = otherPath.appendingPathComponent(key).path 222 | 223 | // When 224 | try standardStorage.save(data1, forKey: key) 225 | 226 | // Then 227 | XCTAssertEqual(fileManager.contents(atPath: filePath1), data1) 228 | XCTAssertNil(fileManager.contents(atPath: filePath2)) 229 | 230 | // When 231 | try otherStorage.save(data2, forKey: key) 232 | 233 | // Then 234 | XCTAssertEqual(fileManager.contents(atPath: filePath1), data1) 235 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 236 | 237 | // When 238 | try standardStorage.save(data2, forKey: key) 239 | 240 | // Then 241 | XCTAssertEqual(fileManager.contents(atPath: filePath1), data2) 242 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 243 | } 244 | 245 | @FileActor 246 | func testFileDelete() throws { 247 | // Given 248 | let data1 = Data([0xAA, 0xBB, 0xCC]) 249 | let data2 = Data([0xDD, 0xEE, 0xFF]) 250 | let key1 = "key1" 251 | let key2 = "key2" 252 | let filePath1 = standardPath.appendingPathComponent(key1).path 253 | let filePath2 = standardPath.appendingPathComponent(key2).path 254 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data1)) 255 | XCTAssertTrue(fileManager.createFile(atPath: filePath2, contents: data2)) 256 | 257 | // When 258 | try standardStorage.delete(forKey: key1) 259 | 260 | // Then 261 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 262 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 263 | 264 | // When 265 | try standardStorage.delete(forKey: key1) 266 | try standardStorage.delete(forKey: key1) 267 | try standardStorage.delete(forKey: key1) 268 | try standardStorage.delete(forKey: key1) 269 | 270 | // Then 271 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 272 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 273 | 274 | // When 275 | try standardStorage.delete(forKey: key2) 276 | 277 | // Then 278 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 279 | XCTAssertNil(fileManager.contents(atPath: filePath2)) 280 | } 281 | 282 | @FileActor 283 | func testFileDeleteDifferentDomains() throws { 284 | // Given 285 | let data1 = Data([0xAA, 0xBB, 0xCC]) 286 | let data2 = Data([0xDD, 0xEE, 0xFF]) 287 | let key = "key" 288 | let filePath1 = standardPath.appendingPathComponent(key).path 289 | let filePath2 = otherPath.appendingPathComponent(key).path 290 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data1)) 291 | XCTAssertTrue(fileManager.createFile(atPath: filePath2, contents: data2)) 292 | 293 | // When 294 | try standardStorage.delete(forKey: key) 295 | 296 | // Then 297 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 298 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 299 | 300 | // When 301 | try standardStorage.delete(forKey: key) 302 | 303 | // Then 304 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 305 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 306 | 307 | // When 308 | try otherStorage.delete(forKey: key) 309 | 310 | // Then 311 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 312 | XCTAssertNil(fileManager.contents(atPath: filePath2)) 313 | } 314 | 315 | @FileActor 316 | func testFileSet() throws { 317 | // Given 318 | let data1 = Data([0xAA, 0xBB, 0xCC]) 319 | let data2 = Data([0xDD, 0xEE, 0xFF]) 320 | let key1 = "key1" 321 | let key2 = "key2" 322 | let filePath1 = standardPath.appendingPathComponent(key1).path 323 | let filePath2 = standardPath.appendingPathComponent(key2).path 324 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data1)) 325 | XCTAssertTrue(fileManager.createFile(atPath: filePath2, contents: data2)) 326 | 327 | // When 328 | try standardStorage.set(data2, forKey: key1) 329 | 330 | // Then 331 | XCTAssertEqual(fileManager.contents(atPath: filePath1), data2) 332 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 333 | 334 | // When 335 | try standardStorage.set(nil, forKey: key1) 336 | 337 | // Then 338 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 339 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 340 | 341 | // When 342 | try standardStorage.set(data1, forKey: key2) 343 | 344 | // Then 345 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 346 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data1) 347 | 348 | // When 349 | try standardStorage.set(nil, forKey: key2) 350 | 351 | // Then 352 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 353 | XCTAssertNil(fileManager.contents(atPath: filePath2)) 354 | } 355 | 356 | @FileActor 357 | func testFileSetDifferentDomains() throws { 358 | // Given 359 | let data1 = Data([0xAA, 0xBB, 0xCC]) 360 | let data2 = Data([0xDD, 0xEE, 0xFF]) 361 | let key = "key" 362 | let filePath1 = standardPath.appendingPathComponent(key).path 363 | let filePath2 = otherPath.appendingPathComponent(key).path 364 | XCTAssertTrue(fileManager.createFile(atPath: filePath1, contents: data1)) 365 | XCTAssertTrue(fileManager.createFile(atPath: filePath2, contents: data2)) 366 | 367 | // When 368 | try standardStorage.set(data2, forKey: key) 369 | 370 | // Then 371 | XCTAssertEqual(fileManager.contents(atPath: filePath1), data2) 372 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 373 | 374 | // When 375 | try standardStorage.set(nil, forKey: key) 376 | 377 | // Then 378 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 379 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data2) 380 | 381 | // When 382 | try otherStorage.set(data1, forKey: key) 383 | 384 | // Then 385 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 386 | XCTAssertEqual(fileManager.contents(atPath: filePath2), data1) 387 | 388 | // When 389 | try otherStorage.set(nil, forKey: key) 390 | 391 | // Then 392 | XCTAssertNil(fileManager.contents(atPath: filePath1)) 393 | XCTAssertNil(fileManager.contents(atPath: filePath2)) 394 | } 395 | 396 | @FileActor 397 | func testFileClear() throws { 398 | // Given 399 | let data1 = Data([0xAA, 0xBB, 0xCC]) 400 | let data2 = Data([0xDD, 0xEE, 0xFF]) 401 | let key1 = "key1" 402 | let key2 = "key2" 403 | let filePath11 = standardPath.appendingPathComponent(key1).path 404 | let filePath12 = standardPath.appendingPathComponent(key2).path 405 | let filePath21 = otherPath.appendingPathComponent(key1).path 406 | let filePath22 = otherPath.appendingPathComponent(key2).path 407 | XCTAssertTrue(fileManager.createFile(atPath: filePath11, contents: data1)) 408 | XCTAssertTrue(fileManager.createFile(atPath: filePath12, contents: data2)) 409 | XCTAssertTrue(fileManager.createFile(atPath: filePath21, contents: data1)) 410 | XCTAssertTrue(fileManager.createFile(atPath: filePath22, contents: data2)) 411 | 412 | // When 413 | try standardStorage.clear() 414 | 415 | // Then 416 | XCTAssertNil(fileManager.contents(atPath: filePath11)) 417 | XCTAssertNil(fileManager.contents(atPath: filePath12)) 418 | XCTAssertEqual(fileManager.contents(atPath: filePath21), data1) 419 | XCTAssertEqual(fileManager.contents(atPath: filePath22), data2) 420 | 421 | // When 422 | try standardStorage.clear() 423 | 424 | // Then 425 | XCTAssertNil(fileManager.contents(atPath: filePath11)) 426 | XCTAssertNil(fileManager.contents(atPath: filePath12)) 427 | XCTAssertEqual(fileManager.contents(atPath: filePath21), data1) 428 | XCTAssertEqual(fileManager.contents(atPath: filePath22), data2) 429 | 430 | // When 431 | XCTAssertTrue(fileManager.createFile(atPath: filePath11, contents: data2)) 432 | XCTAssertTrue(fileManager.createFile(atPath: filePath12, contents: data1)) 433 | try otherStorage.clear() 434 | 435 | // Then 436 | XCTAssertEqual(fileManager.contents(atPath: filePath11), data2) 437 | XCTAssertEqual(fileManager.contents(atPath: filePath12), data1) 438 | XCTAssertNil(fileManager.contents(atPath: filePath21)) 439 | XCTAssertNil(fileManager.contents(atPath: filePath22)) 440 | 441 | // When 442 | try standardStorage.clear() 443 | 444 | // Then 445 | XCTAssertNil(fileManager.contents(atPath: filePath11)) 446 | XCTAssertNil(fileManager.contents(atPath: filePath12)) 447 | XCTAssertNil(fileManager.contents(atPath: filePath21)) 448 | XCTAssertNil(fileManager.contents(atPath: filePath22)) 449 | } 450 | 451 | @FileActor 452 | func testErrorCaseDelete() { 453 | // Given 454 | let mock = FileManagerMock() 455 | let storage = FileStorage(fileManager: mock, root: URL(string: "root")!) 456 | mock.removeItemError = nil 457 | 458 | do { 459 | // When 460 | try storage.delete(forKey: "nonExistingFile") 461 | } catch { 462 | // Then 463 | XCTFail("Unexpected error") 464 | } 465 | 466 | // Given 467 | mock.removeItemError = CocoaError(CocoaError.fileNoSuchFile) 468 | 469 | do { 470 | // When 471 | try storage.delete(forKey: "nonExistingFile") 472 | } catch { 473 | // Then 474 | XCTFail("Unexpected error") 475 | } 476 | 477 | // Given 478 | mock.removeItemError = CocoaError(CocoaError.coderInvalidValue) 479 | 480 | do { 481 | // When 482 | try storage.delete(forKey: "nonExistingFile") 483 | } catch let error as FileStorage.Error { 484 | // Then 485 | if case let .other(innerError as CocoaError) = error { 486 | XCTAssertEqual(innerError.code, CocoaError.coderInvalidValue) 487 | } else { 488 | XCTFail("Unexpected error") 489 | } 490 | } catch { 491 | // Then 492 | XCTFail("Unexpected error") 493 | } 494 | } 495 | 496 | @FileActor 497 | func testErrorCaseSave() { 498 | // Given 499 | let mock = FileManagerMock() 500 | let storage = FileStorage(fileManager: mock, root: URL(string: "root")!) 501 | mock.createDirectoryError = nil 502 | mock.createFileError = nil 503 | 504 | do { 505 | // When 506 | try storage.save(.init(), forKey: "nonExistingFile") 507 | } catch { 508 | // Then 509 | XCTFail("Unexpected error") 510 | } 511 | 512 | // Given 513 | mock.createDirectoryError = CocoaError(CocoaError.fileWriteFileExists) 514 | mock.createFileError = nil 515 | 516 | do { 517 | // When 518 | try storage.save(.init(), forKey: "nonExistingFile") 519 | } catch { 520 | // Then 521 | XCTFail("Unexpected error") 522 | } 523 | 524 | // Given 525 | mock.createDirectoryError = CocoaError(CocoaError.coderInvalidValue) 526 | mock.createFileError = nil 527 | 528 | do { 529 | // When 530 | try storage.save(.init(), forKey: "nonExistingFile") 531 | } catch let error as FileStorage.Error { 532 | // Then 533 | if case let .other(innerError as CocoaError) = error { 534 | XCTAssertEqual(innerError.code, CocoaError.coderInvalidValue) 535 | } else { 536 | XCTFail("Unexpected error") 537 | } 538 | } catch { 539 | // Then 540 | XCTFail("Unexpected error") 541 | } 542 | 543 | // Given 544 | mock.createDirectoryError = nil 545 | mock.createFileError = CocoaError(CocoaError.coderInvalidValue) 546 | 547 | do { 548 | // When 549 | try storage.save(.init(), forKey: "nonExistingFile") 550 | } catch let error as FileStorage.Error { 551 | // Then 552 | if case .failedToSave = error { 553 | // ok 554 | } else { 555 | XCTFail("Unexpected error") 556 | } 557 | } catch { 558 | // Then 559 | XCTFail("Unexpected error") 560 | } 561 | } 562 | 563 | @FileActor 564 | func testThreadSafety() throws { 565 | // Given 566 | let iterations = 5000 567 | let promise = expectation(description: "testThreadSafety") 568 | let group = DispatchGroup() 569 | for _ in 1...iterations { group.enter() } 570 | 571 | // When 572 | DispatchQueue.concurrentPerform(iterations: iterations) { number in 573 | let operation = Int.random(in: 0...4) 574 | let key = "\(Int.random(in: 1000...9999))" 575 | 576 | Task.detached { 577 | do { 578 | switch operation { 579 | case 0: 580 | _ = try await self.standardStorage.fetch(forKey: key) 581 | case 1: 582 | try await self.standardStorage.save(.init(), forKey: key) 583 | case 2: 584 | try await self.standardStorage.delete(forKey: key) 585 | case 3: 586 | try await self.standardStorage.set(Bool.random() ? nil : .init(), forKey: key) 587 | case 4: 588 | try await self.standardStorage.clear() 589 | default: 590 | break 591 | } 592 | } catch { 593 | XCTFail("Unexpected error") 594 | } 595 | 596 | group.leave() 597 | } 598 | } 599 | 600 | group.notify(queue: .main) { 601 | promise.fulfill() 602 | } 603 | 604 | wait(for: [promise], timeout: 5) 605 | } 606 | } 607 | 608 | extension FileManager { 609 | func clearDirectoryContents(url: URL) throws { 610 | if let paths = try? contentsOfDirectory(atPath: url.path) { 611 | for path in paths { 612 | try removeItem(at: url.appendingPathComponent(path)) 613 | } 614 | } 615 | } 616 | } 617 | --------------------------------------------------------------------------------