Merge pull request #104 from hyperoslo/feature/cache-array

Feature: cache array of Cachable objects
This commit is contained in:
Vadym Markov
2017-06-09 09:23:50 +02:00
committed by GitHub
9 changed files with 193 additions and 20 deletions
+16
View File
@@ -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 */,
+29 -2
View File
@@ -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
}
+3 -3
View File
@@ -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()
)