mirror of
https://github.com/hyperoslo/Cache.git
synced 2026-04-07 19:17:36 +00:00
Merge pull request #104 from hyperoslo/feature/cache-array
Feature: cache array of Cachable objects
This commit is contained in:
@@ -106,6 +106,12 @@
|
||||
D5C24DEF1EE8C50B00D2CF22 /* StringCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A19B81EE8246F009F6AEE /* StringCacheTests.swift */; };
|
||||
D5C24DF01EE8C51500D2CF22 /* DiskStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A19BC1EE8246F009F6AEE /* DiskStorageTests.swift */; };
|
||||
D5C24DF11EE8C51500D2CF22 /* MemoryStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A19BD1EE8246F009F6AEE /* MemoryStorageTests.swift */; };
|
||||
D5C24E221EE9D4CF00D2CF22 /* CacheArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */; };
|
||||
D5C24E231EE9D4CF00D2CF22 /* CacheArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */; };
|
||||
D5C24E241EE9D4CF00D2CF22 /* CacheArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */; };
|
||||
D5C24E261EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */; };
|
||||
D5C24E271EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */; };
|
||||
D5C24E281EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */; };
|
||||
D5CE966D1EE475C0002BFE06 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CE966C1EE475C0002BFE06 /* Coding.swift */; };
|
||||
D5CE966E1EE475C0002BFE06 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CE966C1EE475C0002BFE06 /* Coding.swift */; };
|
||||
D5CE966F1EE475C0002BFE06 /* Coding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5CE966C1EE475C0002BFE06 /* Coding.swift */; };
|
||||
@@ -196,6 +202,8 @@
|
||||
D5C24DD91EE8C29D00D2CF22 /* HybridCacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HybridCacheTests.swift; sourceTree = "<group>"; };
|
||||
D5C24DDF1EE8C49800D2CF22 /* TestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelper.swift; sourceTree = "<group>"; };
|
||||
D5C24DE31EE8C4E000D2CF22 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = "<group>"; };
|
||||
D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheArray.swift; sourceTree = "<group>"; };
|
||||
D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheArrayTests.swift; sourceTree = "<group>"; };
|
||||
D5CE966C1EE475C0002BFE06 /* Coding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = "<group>"; };
|
||||
D5CE96851EE691D6002BFE06 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
|
||||
D5CE968A1EE69969002BFE06 /* SpecializedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpecializedCache.swift; sourceTree = "<group>"; };
|
||||
@@ -293,6 +301,7 @@
|
||||
D5CE966C1EE475C0002BFE06 /* Coding.swift */,
|
||||
D5291C191C28220B00B702C9 /* Expiry.swift */,
|
||||
D5291C1A1C28220B00B702C9 /* JSON.swift */,
|
||||
D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */,
|
||||
);
|
||||
path = DataStructures;
|
||||
sourceTree = "<group>";
|
||||
@@ -400,6 +409,7 @@
|
||||
D59A19B01EE8246F009F6AEE /* CodingTests.swift */,
|
||||
D59A19B11EE8246F009F6AEE /* ExpiryTests.swift */,
|
||||
D59A19B21EE8246F009F6AEE /* JSONTests.swift */,
|
||||
D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */,
|
||||
);
|
||||
path = DataStructures;
|
||||
sourceTree = "<group>";
|
||||
@@ -830,6 +840,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BDEDD3651DBCE5D8007416A6 /* Expiry.swift in Sources */,
|
||||
D5C24E241EE9D4CF00D2CF22 /* CacheArray.swift in Sources */,
|
||||
BDEDD3631DBCE5D8007416A6 /* Cachable.swift in Sources */,
|
||||
BDEDD3661DBCE5D8007416A6 /* JSON.swift in Sources */,
|
||||
BDEDD3711DBCE5D8007416A6 /* StorageAware.swift in Sources */,
|
||||
@@ -860,6 +871,7 @@
|
||||
D59A19DC1EE82484009F6AEE /* DataCacheTests.swift in Sources */,
|
||||
D59A19DE1EE82484009F6AEE /* JSONCacheTests.swift in Sources */,
|
||||
D59A19E01EE82484009F6AEE /* StringCacheTests.swift in Sources */,
|
||||
D5C24E281EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */,
|
||||
D59A19E41EE82484009F6AEE /* MemoryStorageTests.swift in Sources */,
|
||||
D59A19DA1EE82484009F6AEE /* ExpiryTests.swift in Sources */,
|
||||
D59A19D81EE82484009F6AEE /* CapsuleTests.swift in Sources */,
|
||||
@@ -885,6 +897,7 @@
|
||||
D59A19CF1EE82482009F6AEE /* DataCacheTests.swift in Sources */,
|
||||
D59A19D11EE82482009F6AEE /* JSONCacheTests.swift in Sources */,
|
||||
D59A19D31EE82482009F6AEE /* StringCacheTests.swift in Sources */,
|
||||
D5C24E261EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */,
|
||||
D59A19D71EE82482009F6AEE /* MemoryStorageTests.swift in Sources */,
|
||||
D59A19CD1EE82482009F6AEE /* ExpiryTests.swift in Sources */,
|
||||
D5C24DDA1EE8C29D00D2CF22 /* HybridCacheTests.swift in Sources */,
|
||||
@@ -907,6 +920,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D5291D941C283CFB00B702C9 /* MemoryStorage.swift in Sources */,
|
||||
D5C24E231EE9D4CF00D2CF22 /* CacheArray.swift in Sources */,
|
||||
D5291D8B1C283CFB00B702C9 /* Expiry.swift in Sources */,
|
||||
D5291D891C283CFB00B702C9 /* Cachable.swift in Sources */,
|
||||
D5291D8C1C283CFB00B702C9 /* JSON.swift in Sources */,
|
||||
@@ -937,6 +951,7 @@
|
||||
D5C24DDE1EE8C45900D2CF22 /* HybridCacheTests.swift in Sources */,
|
||||
D5C24DF01EE8C51500D2CF22 /* DiskStorageTests.swift in Sources */,
|
||||
D5C24DE11EE8C49800D2CF22 /* TestHelper.swift in Sources */,
|
||||
D5C24E271EE9D78900D2CF22 /* CacheArrayTests.swift in Sources */,
|
||||
D5C24DE81EE8C50200D2CF22 /* CodingTests.swift in Sources */,
|
||||
D59A19EA1EE824B4009F6AEE /* NSImageCacheTests.swift in Sources */,
|
||||
D5C24DEE1EE8C50B00D2CF22 /* NSDictionaryCacheTests.swift in Sources */,
|
||||
@@ -959,6 +974,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D5291C2E1C28220B00B702C9 /* Cachable.swift in Sources */,
|
||||
D5C24E221EE9D4CF00D2CF22 /* CacheArray.swift in Sources */,
|
||||
D5291C311C28220B00B702C9 /* JSON.swift in Sources */,
|
||||
D5291C361C28220B00B702C9 /* String+Cache.swift in Sources */,
|
||||
D5291C341C28220B00B702C9 /* Data+Cache.swift in Sources */,
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
* [Optional bonuses](#optional-bonuses)
|
||||
* [JSON](#json)
|
||||
* [Coding](#coding)
|
||||
* [CacheArray](#cachearray)
|
||||
* [What about images?](#what-about-images)
|
||||
* [Installation](#installation)
|
||||
* [Author](#author)
|
||||
@@ -54,6 +55,7 @@ types.
|
||||
implemented for `UIImage`, `String`, `JSON` and `Data`.
|
||||
- [x] Error handling and logs.
|
||||
- [x] `Coding` protocol brings power of `NSCoding` to Swift structs and enums
|
||||
- [x] `CacheArray` allows to cache an array of `Cachable` objects.
|
||||
- [x] Extensive unit test coverage and great documentation.
|
||||
- [x] iOS, tvOS and macOS support.
|
||||
|
||||
@@ -185,6 +187,8 @@ cache["key"] = nil
|
||||
print(cache["key"]) // Prints nil
|
||||
```
|
||||
|
||||
Note that default cache expiry will be used when you use subscript.
|
||||
|
||||
**Sync API**
|
||||
|
||||
```swift
|
||||
@@ -195,10 +199,10 @@ let cache = SpecializedCache<UIImage>(name: "ImageCache")
|
||||
try cache.addObject(UIImage(named: "image.png"), forKey: "image")
|
||||
|
||||
// Get object from cache
|
||||
let image: UIImage? = cache.object(forKey: "image")
|
||||
let image = cache.object(forKey: "image")
|
||||
|
||||
// Get object with expiry date
|
||||
let entry: CacheEntry<String>? = cache.cacheEntry(forKey: "image")
|
||||
let entry = cache.cacheEntry(forKey: "image")
|
||||
print(entry?.object)
|
||||
print(entry?.expiry.date) // Prints expiry date
|
||||
|
||||
@@ -366,6 +370,29 @@ print(object?.title) // Prints title
|
||||
|
||||
```
|
||||
|
||||
### CacheArray
|
||||
|
||||
You can use `CacheArray` to cache an array of `Cachable` objects.
|
||||
|
||||
```swift
|
||||
// SpecializedCache
|
||||
let cache = SpecializedCache<CacheArray<String>>(name: "User")
|
||||
let object = CacheArray(elements: ["string1", "string2"])
|
||||
try cache.addObject(object, forKey: "array")
|
||||
let array = cache.object(forKey: "array")?.elements
|
||||
print(array) // Prints ["string1", "string2"]
|
||||
```
|
||||
|
||||
```swift
|
||||
// HybridCache
|
||||
let cache = HybridCache(name: "Mix")
|
||||
let object = CacheArray(elements: ["string1", "string2"])
|
||||
try cache.addObject(object, forKey: "array")
|
||||
let array = (cache.object(forKey: "array") as CacheArray<String>?)?.elements
|
||||
print(array) // Prints ["string1", "string2"]
|
||||
```
|
||||
|
||||
|
||||
## What about images?
|
||||
|
||||
As being said before, `Cache` works with any kind of `Cachable` types, with no
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import Foundation
|
||||
|
||||
/// A wrapper around array of `Cachable` objects that performs data decoding and encoding.
|
||||
public struct CacheArray<T: Cachable>: Cachable {
|
||||
/// Array of elements
|
||||
public let elements: [T]
|
||||
|
||||
/**
|
||||
Creates an instance of `CacheArray`
|
||||
- Parameter elements: Array of `Cachable` elements
|
||||
*/
|
||||
public init(elements: [T]) {
|
||||
self.elements = elements
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an instance from Data.
|
||||
- Parameter data: Data to decode from
|
||||
- Returns: An optional CacheType
|
||||
*/
|
||||
public static func decode(_ data: Data) -> CacheArray<T>? {
|
||||
// Unarchive object as an array of data
|
||||
guard let dataArray = (NSKeyedUnarchiver.unarchiveObject(with: data) as? NSArray) as? [Data] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
do {
|
||||
// Decode data to element of `T` type.
|
||||
let elements = try dataArray.map ({ data -> T in
|
||||
guard let element = T.decode(data) as? T else {
|
||||
throw Error.decodingFailed
|
||||
}
|
||||
return element
|
||||
})
|
||||
return CacheArray(elements: elements)
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Encodes an instance to Data.
|
||||
- Returns: Optional Data
|
||||
*/
|
||||
public func encode() -> Data? {
|
||||
do {
|
||||
// Create an array of data to be able to archive as `NSArray`
|
||||
let dataArray = try elements.map ({ element -> Data in
|
||||
guard let data = element.encode() else {
|
||||
throw Error.encodingFailed
|
||||
}
|
||||
return data
|
||||
})
|
||||
return NSKeyedArchiver.archivedData(withRootObject: NSArray(array: dataArray))
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum Error: Swift.Error {
|
||||
case encodingFailed
|
||||
case decodingFailed
|
||||
}
|
||||
@@ -31,7 +31,7 @@ final class HybridCacheTests: XCTestCase {
|
||||
|
||||
// MARK: - Async caching
|
||||
|
||||
func testAsyncAddObject() {
|
||||
func testAsyncAddObject() throws {
|
||||
let expectation1 = self.expectation(description: "Save Expectation")
|
||||
let expectation2 = self.expectation(description: "Save To Memory Expectation")
|
||||
let expectation3 = self.expectation(description: "Save To Disk Expectation")
|
||||
@@ -92,10 +92,10 @@ final class HybridCacheTests: XCTestCase {
|
||||
}
|
||||
|
||||
/// Should resolve from disk and set in-memory cache if object not in-memory
|
||||
func testAsyncObjectCopyToMemory() {
|
||||
func testAsyncObjectCopyToMemory() throws {
|
||||
let expectation = self.expectation(description: "Expectation")
|
||||
|
||||
try! cache.manager.backStorage.addObject(object, forKey: key)
|
||||
try cache.manager.backStorage.addObject(object, forKey: key)
|
||||
cache.async.object(forKey: key) { (cachedObject: String?) in
|
||||
XCTAssertNotNil(cachedObject)
|
||||
XCTAssertEqual(cachedObject, self.object)
|
||||
|
||||
@@ -31,7 +31,7 @@ final class SpecializedCacheTests: XCTestCase {
|
||||
|
||||
// MARK: - Async caching
|
||||
|
||||
func testAsyncAddObject() {
|
||||
func testAsyncAddObject() throws {
|
||||
let expectation1 = self.expectation(description: "Save Expectation")
|
||||
let expectation2 = self.expectation(description: "Save To Memory Expectation")
|
||||
let expectation3 = self.expectation(description: "Save To Disk Expectation")
|
||||
@@ -94,10 +94,10 @@ final class SpecializedCacheTests: XCTestCase {
|
||||
}
|
||||
|
||||
/// Should resolve from disk and set in-memory cache if object not in-memory
|
||||
func testAsyncObjectCopyToMemory() {
|
||||
func testAsyncObjectCopyToMemory() throws {
|
||||
let expectation = self.expectation(description: "Expectation")
|
||||
|
||||
try! cache.manager.backStorage.addObject(object, forKey: key)
|
||||
try cache.manager.backStorage.addObject(object, forKey: key)
|
||||
cache.async.object(forKey: key) { cachedObject in
|
||||
XCTAssertNotNil(cachedObject)
|
||||
XCTAssertEqual(cachedObject?.firstName, self.object.firstName)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import XCTest
|
||||
@testable import Cache
|
||||
|
||||
// MARK: - Test case
|
||||
|
||||
final class CacheArrayTests: XCTestCase {
|
||||
private let fileManager = FileManager()
|
||||
|
||||
/// Test encoding and decoding
|
||||
func testDiskStorageWithStringArray() throws {
|
||||
let storage = DiskStorage(name: "Storage")
|
||||
let array: [String] = ["Test1", "Test2"]
|
||||
|
||||
try storage.addObject(CacheArray(elements: array), forKey: "key")
|
||||
let cachedObject: CacheArray<String>? = try storage.object(forKey: "key")
|
||||
|
||||
XCTAssertEqual(cachedObject?.elements.count, 2)
|
||||
XCTAssertEqual(cachedObject?.elements[0], "Test1")
|
||||
XCTAssertEqual(cachedObject?.elements[1], "Test2")
|
||||
|
||||
// Cleanup
|
||||
try fileManager.removeItem(atPath: storage.path)
|
||||
}
|
||||
|
||||
/// Test encoding and decoding
|
||||
func testDiskStorageWithCodingArray() throws {
|
||||
let storage = DiskStorage(name: "Storage")
|
||||
let array: [User] = [
|
||||
User(firstName: "First1", lastName: "Last1"),
|
||||
User(firstName: "First2", lastName: "Last2")
|
||||
]
|
||||
|
||||
try storage.addObject(CacheArray(elements: array), forKey: "key")
|
||||
let cachedObject: CacheArray<User>? = try storage.object(forKey: "key")
|
||||
|
||||
XCTAssertEqual(cachedObject?.elements.count, 2)
|
||||
XCTAssertEqual(cachedObject?.elements[0].firstName, "First1")
|
||||
XCTAssertEqual(cachedObject?.elements[0].lastName, "Last1")
|
||||
XCTAssertEqual(cachedObject?.elements[1].firstName, "First2")
|
||||
XCTAssertEqual(cachedObject?.elements[1].lastName, "Last2")
|
||||
|
||||
// Cleanup
|
||||
try fileManager.removeItem(atPath: storage.path)
|
||||
}
|
||||
|
||||
/// Test encoding and decoding
|
||||
func testSpecializedCacheWithCodingArray() throws {
|
||||
let cache = SpecializedCache<CacheArray<User>>(name: "Cache")
|
||||
let array: [User] = [
|
||||
User(firstName: "First1", lastName: "Last1"),
|
||||
User(firstName: "First2", lastName: "Last2")
|
||||
]
|
||||
|
||||
cache["key"] = CacheArray(elements: array)
|
||||
let cachedObject = cache["key"]
|
||||
|
||||
XCTAssertEqual(cachedObject?.elements.count, 2)
|
||||
XCTAssertEqual(cachedObject?.elements[0].firstName, "First1")
|
||||
XCTAssertEqual(cachedObject?.elements[0].lastName, "Last1")
|
||||
XCTAssertEqual(cachedObject?.elements[1].firstName, "First2")
|
||||
XCTAssertEqual(cachedObject?.elements[1].lastName, "Last2")
|
||||
|
||||
// Cleanup
|
||||
try cache.clear()
|
||||
}
|
||||
}
|
||||
@@ -21,9 +21,9 @@ final class CodingTests: XCTestCase {
|
||||
}
|
||||
|
||||
/// Test encoding and decoding
|
||||
func testCoding() {
|
||||
try! storage.addObject(object, forKey: key)
|
||||
let cachedObject: User? = try! storage.object(forKey: key)
|
||||
func testCoding() throws {
|
||||
try storage.addObject(object, forKey: key)
|
||||
let cachedObject: User? = try storage.object(forKey: key)
|
||||
XCTAssertEqual(cachedObject?.firstName, "First")
|
||||
XCTAssertEqual(cachedObject?.lastName, "Last")
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ final class ExpiryTests: XCTestCase {
|
||||
XCTAssertEqualWithAccuracy(
|
||||
expiry.date.timeIntervalSinceReferenceDate,
|
||||
date.timeIntervalSinceReferenceDate,
|
||||
accuracy: 0.01
|
||||
accuracy: 0.1
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import XCTest
|
||||
|
||||
final class JSONCacheTests: XCTestCase {
|
||||
/// Test that it decodes a dictionary from NSData
|
||||
func testDecodeWithDictionary() {
|
||||
func testDecodeWithDictionary() throws {
|
||||
let object = ["key": "value"]
|
||||
let data = try! JSONSerialization.data(
|
||||
let data = try JSONSerialization.data(
|
||||
withJSONObject: object,
|
||||
options: JSONSerialization.WritingOptions()
|
||||
)
|
||||
@@ -21,9 +21,9 @@ final class JSONCacheTests: XCTestCase {
|
||||
}
|
||||
|
||||
/// Test that it decodes an array from NSData
|
||||
func testDecodeWithArray() {
|
||||
func testDecodeWithArray() throws {
|
||||
let object = ["value1", "value2", "value3"]
|
||||
let data = try! JSONSerialization.data(
|
||||
let data = try JSONSerialization.data(
|
||||
withJSONObject: object,
|
||||
options: JSONSerialization.WritingOptions()
|
||||
)
|
||||
@@ -40,9 +40,9 @@ final class JSONCacheTests: XCTestCase {
|
||||
}
|
||||
|
||||
/// Test that it encodes a dictionary to NSData
|
||||
func testEncodeWithDictionary() {
|
||||
func testEncodeWithDictionary() throws {
|
||||
let object = ["key": "value"]
|
||||
let data = try! JSONSerialization.data(
|
||||
let data = try JSONSerialization.data(
|
||||
withJSONObject: object,
|
||||
options: JSONSerialization.WritingOptions()
|
||||
)
|
||||
@@ -52,9 +52,9 @@ final class JSONCacheTests: XCTestCase {
|
||||
}
|
||||
|
||||
/// Test that it encodes an array to NSData
|
||||
func testEncodeWithArray() {
|
||||
func testEncodeWithArray() throws {
|
||||
let object = ["value1", "value2", "value3"]
|
||||
let data = try! JSONSerialization.data(
|
||||
let data = try JSONSerialization.data(
|
||||
withJSONObject: object,
|
||||
options: JSONSerialization.WritingOptions()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user