Skip to content

Commit

Permalink
Update PersistableCache to use mapppings (#13)
Browse files Browse the repository at this point in the history
* Update PersistableCache to use mapppings

* Update test for macos and other os

* Update sleep for potential flaky test
  • Loading branch information
0xLeif committed Jul 22, 2023
1 parent d6d6325 commit a535c68
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 26 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Double>()
enum Key: String {
case pi
}

let cache = PersistableCache<Key, Double, Double>()

cache["pi"] = Double.pi
cache[.pi] = Double.pi

do {
try cache.save()
Expand All @@ -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<String, Double>()
let cache = PersistableCache<Key, Double, Double>()

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.
Expand Down
69 changes: 57 additions & 12 deletions Sources/Cache/Cache/PersistableCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Double>()
enum Key: String {
case pi
}
let cache = PersistableCache<Key, Double, Double>()
cache["pi"] = Double.pi
cache[.pi] = Double.pi
do {
try cache.save()
Expand All @@ -23,9 +27,9 @@ import Foundation
You can also load a previously saved cache from disk:
```swift
let cache = PersistableCache<String, Double>()
let cache = PersistableCache<Key, Double, Double>()
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.
Expand All @@ -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<Key, Value> where Key.RawValue == String {
private let lock: NSLock = NSLock()

Expand All @@ -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<Key>(data: fileData)
initialValues = loadedJSON.values(ofType: Value.self)
initialValues = loadedJSON
.values(ofType: PersistedValue.self)
.compactMapValues(cachedValueMap)
}

super.init(initialValues: initialValues)
Expand All @@ -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 }
)
}

Expand All @@ -90,14 +107,41 @@ 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
set(value: value, forKey: key)
}
}

/**
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.
Expand All @@ -107,7 +151,8 @@ open class PersistableCache<
*/
public func save() throws {
lock.lock()
let json = JSON<Key>(initialValues: allValues)
let persistedValues = allValues.compactMapValues(persistedValueMap)
let json = JSON<Key>(initialValues: persistedValues)
let data = try json.data()
try data.write(to: url.fileURL(withName: name))
lock.unlock()
Expand Down
2 changes: 1 addition & 1 deletion Tests/CacheTests/ComposableCacheTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ final class ComposableCacheTests: XCTestCase {
XCTAssertNotNil(cache.get(.c))
XCTAssertNotNil(cache.get(.d))

sleep(1)
sleep(2)

// Check ComposableCache

Expand Down
108 changes: 99 additions & 9 deletions Tests/CacheTests/PersistableCacheTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
#if !os(Linux) && !os(Windows)
#if os(macOS)
import AppKit
#else
import UIKit
#endif

import XCTest
@testable import Cache

Expand All @@ -9,7 +15,7 @@ final class PersistableCacheTests: XCTestCase {
case author
}

let cache: PersistableCache<Key, String> = PersistableCache(
let cache: PersistableCache<Key, String, String> = PersistableCache(
initialValues: [
.text: "Hello, World!"
]
Expand All @@ -24,13 +30,13 @@ final class PersistableCacheTests: XCTestCase {
case text
}

let failedLoadedCache: PersistableCache<SomeOtherKey, String> = PersistableCache()
let failedLoadedCache: PersistableCache<SomeOtherKey, String, String> = PersistableCache()

XCTAssertEqual(failedLoadedCache.allValues.count, 0)
XCTAssertEqual(failedLoadedCache.url, cache.url)
XCTAssertNotEqual(failedLoadedCache.name, cache.name)

let loadedCache: PersistableCache<Key, String> = PersistableCache(
let loadedCache: PersistableCache<Key, String, String> = PersistableCache(
initialValues: [
.author: "Leif"
]
Expand All @@ -40,11 +46,11 @@ final class PersistableCacheTests: XCTestCase {

try loadedCache.delete()

let loadedDeletedCache: PersistableCache<Key, String> = PersistableCache()
let loadedDeletedCache: PersistableCache<Key, String, String> = PersistableCache()

XCTAssertEqual(loadedDeletedCache.allValues.count, 0)

let expectedName = "PersistableCache<Key, String>"
let expectedName = "PersistableCache<Key, String, String>"
let expectedURL = FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask
Expand All @@ -67,15 +73,15 @@ final class PersistableCacheTests: XCTestCase {
case author
}

let cache: PersistableCache<Key, String> = PersistableCache(name: "test")
let cache: PersistableCache<Key, String, String> = PersistableCache(name: "test")

cache[.text] = "Hello, World!"

XCTAssertEqual(cache.allValues.count, 1)

try cache.save()

let loadedCache: PersistableCache<Key, String> = PersistableCache(name: "test")
let loadedCache: PersistableCache<Key, String, String> = PersistableCache(name: "test")

loadedCache[.author] = "Leif"

Expand All @@ -87,7 +93,7 @@ final class PersistableCacheTests: XCTestCase {
case text
}

let otherKeyedLoadedCache: PersistableCache<SomeOtherKey, String> = PersistableCache(name: "test")
let otherKeyedLoadedCache: PersistableCache<SomeOtherKey, String, String> = PersistableCache(name: "test")

XCTAssertEqual(otherKeyedLoadedCache.allValues.count, 1)
XCTAssertEqual(otherKeyedLoadedCache.url, cache.url)
Expand All @@ -97,7 +103,7 @@ final class PersistableCacheTests: XCTestCase {

try loadedCache.delete()

let loadedDeletedCache: PersistableCache<Key, String> = PersistableCache(name: "test")
let loadedDeletedCache: PersistableCache<Key, String, String> = PersistableCache(name: "test")

XCTAssertEqual(loadedDeletedCache.allValues.count, 0)

Expand All @@ -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<Key, NSImage, String> = 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<Key, NSImage, String> = 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<Key, UIImage, String> = 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<Key, UIImage, String> = 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

0 comments on commit a535c68

Please sign in to comment.