From b713cdc984919aa1f15b47444da7b07fbcb12e5b Mon Sep 17 00:00:00 2001 From: Vadym Markov Date: Thu, 8 Jun 2017 21:46:22 +0200 Subject: [PATCH 1/3] Implement CacheArray --- Cache.xcodeproj/project.pbxproj | 16 +++++ Source/Shared/DataStructures/CacheArray.swift | 64 ++++++++++++++++++ Tests/iOS/Tests/Cache/HybridCacheTests.swift | 6 +- .../Tests/Cache/SpecializedCacheTests.swift | 6 +- .../DataStructures/CacheArrayTests.swift | 66 +++++++++++++++++++ .../Tests/DataStructures/CodingTests.swift | 6 +- .../Tests/DataStructures/ExpiryTests.swift | 2 +- .../iOS/Tests/Extensions/JSONCacheTests.swift | 16 ++--- 8 files changed, 164 insertions(+), 18 deletions(-) create mode 100644 Source/Shared/DataStructures/CacheArray.swift create mode 100644 Tests/iOS/Tests/DataStructures/CacheArrayTests.swift diff --git a/Cache.xcodeproj/project.pbxproj b/Cache.xcodeproj/project.pbxproj index eeb66ca..9aef237 100644 --- a/Cache.xcodeproj/project.pbxproj +++ b/Cache.xcodeproj/project.pbxproj @@ -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 = ""; }; D5C24DDF1EE8C49800D2CF22 /* TestHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestHelper.swift; sourceTree = ""; }; D5C24DE31EE8C4E000D2CF22 /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheArray.swift; sourceTree = ""; }; + D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheArrayTests.swift; sourceTree = ""; }; D5CE966C1EE475C0002BFE06 /* Coding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Coding.swift; sourceTree = ""; }; D5CE96851EE691D6002BFE06 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; D5CE968A1EE69969002BFE06 /* SpecializedCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SpecializedCache.swift; sourceTree = ""; }; @@ -293,6 +301,7 @@ D5CE966C1EE475C0002BFE06 /* Coding.swift */, D5291C191C28220B00B702C9 /* Expiry.swift */, D5291C1A1C28220B00B702C9 /* JSON.swift */, + D5C24E211EE9D4CF00D2CF22 /* CacheArray.swift */, ); path = DataStructures; sourceTree = ""; @@ -400,6 +409,7 @@ D59A19B01EE8246F009F6AEE /* CodingTests.swift */, D59A19B11EE8246F009F6AEE /* ExpiryTests.swift */, D59A19B21EE8246F009F6AEE /* JSONTests.swift */, + D5C24E251EE9D78900D2CF22 /* CacheArrayTests.swift */, ); path = DataStructures; sourceTree = ""; @@ -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 */, diff --git a/Source/Shared/DataStructures/CacheArray.swift b/Source/Shared/DataStructures/CacheArray.swift new file mode 100644 index 0000000..c5bfca6 --- /dev/null +++ b/Source/Shared/DataStructures/CacheArray.swift @@ -0,0 +1,64 @@ +import Foundation + +/// A wrapper around array of `Cachable` objects that performs data decoding and encoding. +public struct CacheArray: Cachable { + private enum Error: Swift.Error { + case encodingFailed + case decodingFailed + } + + /// 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? { + // 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 + } + } +} diff --git a/Tests/iOS/Tests/Cache/HybridCacheTests.swift b/Tests/iOS/Tests/Cache/HybridCacheTests.swift index fe54a35..cc20f21 100644 --- a/Tests/iOS/Tests/Cache/HybridCacheTests.swift +++ b/Tests/iOS/Tests/Cache/HybridCacheTests.swift @@ -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) diff --git a/Tests/iOS/Tests/Cache/SpecializedCacheTests.swift b/Tests/iOS/Tests/Cache/SpecializedCacheTests.swift index 70072c5..110280a 100644 --- a/Tests/iOS/Tests/Cache/SpecializedCacheTests.swift +++ b/Tests/iOS/Tests/Cache/SpecializedCacheTests.swift @@ -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) diff --git a/Tests/iOS/Tests/DataStructures/CacheArrayTests.swift b/Tests/iOS/Tests/DataStructures/CacheArrayTests.swift new file mode 100644 index 0000000..6631c80 --- /dev/null +++ b/Tests/iOS/Tests/DataStructures/CacheArrayTests.swift @@ -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? = 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? = 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>(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() + } +} diff --git a/Tests/iOS/Tests/DataStructures/CodingTests.swift b/Tests/iOS/Tests/DataStructures/CodingTests.swift index 8567e64..da91d3c 100644 --- a/Tests/iOS/Tests/DataStructures/CodingTests.swift +++ b/Tests/iOS/Tests/DataStructures/CodingTests.swift @@ -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") } diff --git a/Tests/iOS/Tests/DataStructures/ExpiryTests.swift b/Tests/iOS/Tests/DataStructures/ExpiryTests.swift index bba7f6b..002bec6 100644 --- a/Tests/iOS/Tests/DataStructures/ExpiryTests.swift +++ b/Tests/iOS/Tests/DataStructures/ExpiryTests.swift @@ -18,7 +18,7 @@ final class ExpiryTests: XCTestCase { XCTAssertEqualWithAccuracy( expiry.date.timeIntervalSinceReferenceDate, date.timeIntervalSinceReferenceDate, - accuracy: 0.01 + accuracy: 0.1 ) } diff --git a/Tests/iOS/Tests/Extensions/JSONCacheTests.swift b/Tests/iOS/Tests/Extensions/JSONCacheTests.swift index 2136fc9..8bb5ff1 100644 --- a/Tests/iOS/Tests/Extensions/JSONCacheTests.swift +++ b/Tests/iOS/Tests/Extensions/JSONCacheTests.swift @@ -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() ) From fb728c2fe7fd1d325e5c9eee3c21989bb99c3efd Mon Sep 17 00:00:00 2001 From: Vadym Markov Date: Thu, 8 Jun 2017 22:02:06 +0200 Subject: [PATCH 2/3] Update readme --- README.md | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index da8e0ab..da598c0 100644 --- a/README.md +++ b/README.md @@ -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(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? = 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>(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?)?.elements +print(array) // Prints ["string1", "string2"] +``` + + ## What about images? As being said before, `Cache` works with any kind of `Cachable` types, with no From dbcdeac078a7fa311b14bfb3d032d2df6c8410e2 Mon Sep 17 00:00:00 2001 From: Vadym Markov Date: Thu, 8 Jun 2017 23:52:53 +0200 Subject: [PATCH 3/3] Fix provate Error type --- Source/Shared/DataStructures/CacheArray.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Source/Shared/DataStructures/CacheArray.swift b/Source/Shared/DataStructures/CacheArray.swift index c5bfca6..ad4f63d 100644 --- a/Source/Shared/DataStructures/CacheArray.swift +++ b/Source/Shared/DataStructures/CacheArray.swift @@ -2,11 +2,6 @@ import Foundation /// A wrapper around array of `Cachable` objects that performs data decoding and encoding. public struct CacheArray: Cachable { - private enum Error: Swift.Error { - case encodingFailed - case decodingFailed - } - /// Array of elements public let elements: [T] @@ -62,3 +57,8 @@ public struct CacheArray: Cachable { } } } + +private enum Error: Swift.Error { + case encodingFailed + case decodingFailed +}