From a535c68aab7bf0b42bda7a5de66b04f86e48c6e6 Mon Sep 17 00:00:00 2001 From: Zach Date: Sat, 22 Jul 2023 12:38:07 -0500 Subject: [PATCH] Update PersistableCache to use mapppings (#13) * Update PersistableCache to use mapppings * Update test for macos and other os * Update sleep for potential flaky test --- README.md | 12 ++- Sources/Cache/Cache/PersistableCache.swift | 69 +++++++++--- Tests/CacheTests/ComposableCacheTests.swift | 2 +- Tests/CacheTests/PersistableCacheTests.swift | 108 +++++++++++++++++-- 4 files changed, 165 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index eb3e28d..d38ab8d 100644 --- a/README.md +++ b/README.md @@ -117,9 +117,13 @@ To use `PersistableCache`, make sure that the specified key type conforms to bot Here's an example of creating a cache, setting a value, and saving it to disk: ```swift - let cache = PersistableCache() + enum Key: String { + case pi + } + + let cache = PersistableCache() - cache["pi"] = Double.pi + cache[.pi] = Double.pi do { try cache.save() @@ -131,9 +135,9 @@ To use `PersistableCache`, make sure that the specified key type conforms to bot You can also load a previously saved cache from disk: ```swift - let cache = PersistableCache() + let cache = PersistableCache() - let pi = cache["pi"] // pi == Double.pi + let pi = cache[.pi] // pi == Double.pi ``` Remember that the `save()` function may throw errors if the encoder fails to serialize the cache to JSON or the disk write operation fails. Make sure to handle the errors appropriately. diff --git a/Sources/Cache/Cache/PersistableCache.swift b/Sources/Cache/Cache/PersistableCache.swift index 0beca3a..2536f03 100644 --- a/Sources/Cache/Cache/PersistableCache.swift +++ b/Sources/Cache/Cache/PersistableCache.swift @@ -9,9 +9,13 @@ import Foundation Here's an example of creating a cache, setting a value, and saving it to disk: ```swift - let cache = PersistableCache() + enum Key: String { + case pi + } + + let cache = PersistableCache() - cache["pi"] = Double.pi + cache[.pi] = Double.pi do { try cache.save() @@ -23,9 +27,9 @@ import Foundation You can also load a previously saved cache from disk: ```swift - let cache = PersistableCache() + let cache = PersistableCache() - let pi = cache["pi"] // pi == Double.pi + let pi = cache[.pi] // pi == Double.pi ``` Note: You must make sure that the specified key type conforms to both `RawRepresentable` and `Hashable` protocols. The `RawValue` of `Key` must be a `String` type. @@ -36,8 +40,8 @@ import Foundation Make sure to handle the errors appropriately. */ -open class PersistableCache< - Key: RawRepresentable & Hashable, Value +public class PersistableCache< + Key: RawRepresentable & Hashable, Value, PersistedValue >: Cache where Key.RawValue == String { private let lock: NSLock = NSLock() @@ -47,25 +51,36 @@ open class PersistableCache< /// The URL of the persistable cache file's directory. public let url: URL + private let persistedValueMap: (Value) -> PersistedValue? + private let cachedValueMap: (PersistedValue) -> Value? + /** Loads a persistable cache with a specified name and URL. - Parameters: - name: A string specifying the name of the cache. - url: A URL where the cache file directory will be or is stored. + - persistedValueMap: A closure that maps the cached value to the `PersistedValue`. + - cachedValueMap: A closure that maps the `PersistedValue` to`Value`. */ public init( name: String, - url: URL + url: URL, + persistedValueMap: @escaping (Value) -> PersistedValue?, + cachedValueMap: @escaping (PersistedValue) -> Value? ) { self.name = name self.url = url + self.persistedValueMap = persistedValueMap + self.cachedValueMap = cachedValueMap var initialValues: [Key: Value] = [:] if let fileData = try? Data(contentsOf: url.fileURL(withName: name)) { let loadedJSON = JSON(data: fileData) - initialValues = loadedJSON.values(ofType: Value.self) + initialValues = loadedJSON + .values(ofType: PersistedValue.self) + .compactMapValues(cachedValueMap) } super.init(initialValues: initialValues) @@ -78,10 +93,12 @@ open class PersistableCache< */ public convenience init( name: String - ) { + ) where Value == PersistedValue { self.init( name: name, - url: URL.defaultFileURL + url: URL.defaultFileURL, + persistedValueMap: { $0 }, + cachedValueMap: { $0 } ) } @@ -90,7 +107,7 @@ open class PersistableCache< - Parameter initialValues: A dictionary containing the initial cache contents. */ - public required convenience init(initialValues: [Key: Value] = [:]) { + public required convenience init(initialValues: [Key: Value] = [:]) where Value == PersistedValue { self.init(name: "\(Self.self)") initialValues.forEach { key, value in @@ -98,6 +115,33 @@ open class PersistableCache< } } + /** + Loads the persistable cache with the given initial values. The `name` is set to `"\(Self.self)"`. + + - Parameters: + - initialValues: A dictionary containing the initial cache contents. + - persistedValueMap: A closure that maps the cached value to the `PersistedValue`. + - cachedValueMap: A closure that maps the `PersistedValue` to`Value`. + */ + public convenience init( + initialValues: [Key: Value] = [:], + persistedValueMap: @escaping (Value) -> PersistedValue?, + cachedValueMap: @escaping (PersistedValue) -> Value? + ) { + self.init( + name: "\(Self.self)", + url: URL.defaultFileURL, + persistedValueMap: persistedValueMap, + cachedValueMap: cachedValueMap + ) + + initialValues.forEach { key, value in + set(value: value, forKey: key) + } + } + + required init(initialValues: [Key: Value] = [:]) { fatalError("init(initialValues:) has not been implemented") } + /** Saves the cache contents to disk. @@ -107,7 +151,8 @@ open class PersistableCache< */ public func save() throws { lock.lock() - let json = JSON(initialValues: allValues) + let persistedValues = allValues.compactMapValues(persistedValueMap) + let json = JSON(initialValues: persistedValues) let data = try json.data() try data.write(to: url.fileURL(withName: name)) lock.unlock() diff --git a/Tests/CacheTests/ComposableCacheTests.swift b/Tests/CacheTests/ComposableCacheTests.swift index 51895b9..3840989 100644 --- a/Tests/CacheTests/ComposableCacheTests.swift +++ b/Tests/CacheTests/ComposableCacheTests.swift @@ -42,7 +42,7 @@ final class ComposableCacheTests: XCTestCase { XCTAssertNotNil(cache.get(.c)) XCTAssertNotNil(cache.get(.d)) - sleep(1) + sleep(2) // Check ComposableCache diff --git a/Tests/CacheTests/PersistableCacheTests.swift b/Tests/CacheTests/PersistableCacheTests.swift index 127a58f..6b97dfc 100644 --- a/Tests/CacheTests/PersistableCacheTests.swift +++ b/Tests/CacheTests/PersistableCacheTests.swift @@ -1,4 +1,10 @@ #if !os(Linux) && !os(Windows) +#if os(macOS) +import AppKit +#else +import UIKit +#endif + import XCTest @testable import Cache @@ -9,7 +15,7 @@ final class PersistableCacheTests: XCTestCase { case author } - let cache: PersistableCache = PersistableCache( + let cache: PersistableCache = PersistableCache( initialValues: [ .text: "Hello, World!" ] @@ -24,13 +30,13 @@ final class PersistableCacheTests: XCTestCase { case text } - let failedLoadedCache: PersistableCache = PersistableCache() + let failedLoadedCache: PersistableCache = PersistableCache() XCTAssertEqual(failedLoadedCache.allValues.count, 0) XCTAssertEqual(failedLoadedCache.url, cache.url) XCTAssertNotEqual(failedLoadedCache.name, cache.name) - let loadedCache: PersistableCache = PersistableCache( + let loadedCache: PersistableCache = PersistableCache( initialValues: [ .author: "Leif" ] @@ -40,11 +46,11 @@ final class PersistableCacheTests: XCTestCase { try loadedCache.delete() - let loadedDeletedCache: PersistableCache = PersistableCache() + let loadedDeletedCache: PersistableCache = PersistableCache() XCTAssertEqual(loadedDeletedCache.allValues.count, 0) - let expectedName = "PersistableCache" + let expectedName = "PersistableCache" let expectedURL = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask @@ -67,7 +73,7 @@ final class PersistableCacheTests: XCTestCase { case author } - let cache: PersistableCache = PersistableCache(name: "test") + let cache: PersistableCache = PersistableCache(name: "test") cache[.text] = "Hello, World!" @@ -75,7 +81,7 @@ final class PersistableCacheTests: XCTestCase { try cache.save() - let loadedCache: PersistableCache = PersistableCache(name: "test") + let loadedCache: PersistableCache = PersistableCache(name: "test") loadedCache[.author] = "Leif" @@ -87,7 +93,7 @@ final class PersistableCacheTests: XCTestCase { case text } - let otherKeyedLoadedCache: PersistableCache = PersistableCache(name: "test") + let otherKeyedLoadedCache: PersistableCache = PersistableCache(name: "test") XCTAssertEqual(otherKeyedLoadedCache.allValues.count, 1) XCTAssertEqual(otherKeyedLoadedCache.url, cache.url) @@ -97,7 +103,7 @@ final class PersistableCacheTests: XCTestCase { try loadedCache.delete() - let loadedDeletedCache: PersistableCache = PersistableCache(name: "test") + let loadedDeletedCache: PersistableCache = PersistableCache(name: "test") XCTAssertEqual(loadedDeletedCache.allValues.count, 0) @@ -117,5 +123,89 @@ final class PersistableCacheTests: XCTestCase { [URL](repeating: expectedURL, count: 4) ) } + + #if os(macOS) + func testImage() throws { + enum Key: String { + case image + } + + let cache: PersistableCache = PersistableCache( + initialValues: [ + .image: try XCTUnwrap(NSImage(systemSymbolName: "circle", accessibilityDescription: nil)) + ], + persistedValueMap: { image in + image.tiffRepresentation?.base64EncodedString() + }, + cachedValueMap: { string in + guard let data = Data(base64Encoded: string) else { + return nil + } + + return NSImage(data: data) + } + ) + + XCTAssertEqual(cache.allValues.count, 1) + + try cache.save() + + let loadedCache: PersistableCache = PersistableCache( + persistedValueMap: { image in + image.tiffRepresentation?.base64EncodedString() + }, + cachedValueMap: { string in + guard let data = Data(base64Encoded: string) else { + return nil + } + + return NSImage(data: data) + } + ) + + XCTAssertEqual(loadedCache.allValues.count, 1) + } + #else + func testImage() throws { + enum Key: String { + case image + } + + let cache: PersistableCache = PersistableCache( + initialValues: [ + .image: try XCTUnwrap(UIImage(systemName: "circle")) + ], + persistedValueMap: { image in + image.pngData()?.base64EncodedString() + }, + cachedValueMap: { string in + guard let data = Data(base64Encoded: string) else { + return nil + } + + return UIImage(data: data) + } + ) + + XCTAssertEqual(cache.allValues.count, 1) + + try cache.save() + + let loadedCache: PersistableCache = PersistableCache( + persistedValueMap: { image in + image.pngData()?.base64EncodedString() + }, + cachedValueMap: { string in + guard let data = Data(base64Encoded: string) else { + return nil + } + + return UIImage(data: data) + } + ) + + XCTAssertEqual(loadedCache.allValues.count, 1) + } + #endif } #endif