diff --git a/Cache.xcodeproj/project.pbxproj b/Cache.xcodeproj/project.pbxproj index dac1492..545b326 100644 --- a/Cache.xcodeproj/project.pbxproj +++ b/Cache.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 57506FAE1EC29437009B71E9 /* CacheEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* CacheEntry.swift */; }; + 57506FBF1EC2E1B7009B71E9 /* CacheEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* CacheEntry.swift */; }; + 57506FC01EC2E1BA009B71E9 /* CacheEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* CacheEntry.swift */; }; BDEDD3601DBCE5CE007416A6 /* BasicHybridCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5291C6A1C2827FB00B702C9 /* BasicHybridCache.swift */; }; BDEDD3611DBCE5CE007416A6 /* Cache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACACDD1CD0272600567809 /* Cache.swift */; }; BDEDD3621DBCE5CE007416A6 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5291C151C28220B00B702C9 /* Config.swift */; }; @@ -143,6 +146,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 57506FAD1EC29437009B71E9 /* CacheEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheEntry.swift; sourceTree = ""; }; BDEDD3561DBCE5B1007416A6 /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BDEDD3781DBCEB8A007416A6 /* Cache-tvOS-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Cache-tvOS-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; D5291C151C28220B00B702C9 /* Config.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; @@ -196,7 +200,7 @@ D5ACACCB1CD0207300567809 /* SyncHybridCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncHybridCache.swift; sourceTree = ""; }; D5ACACCE1CD0227200567809 /* SyncCacheSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncCacheSpec.swift; sourceTree = ""; }; D5ACACD21CD0254300567809 /* SyncHybridCacheSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyncHybridCacheSpec.swift; sourceTree = ""; }; - D5ACACDD1CD0272600567809 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; }; + D5ACACDD1CD0272600567809 /* Cache.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = Cache.swift; sourceTree = ""; tabWidth = 2; }; D5DC59E01C20593E003BD79B /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -322,6 +326,7 @@ D5291C251C28220B00B702C9 /* MemoryStorage.swift */, D5291C261C28220B00B702C9 /* StorageAware.swift */, D5291C271C28220B00B702C9 /* StorageFactory.swift */, + 57506FAD1EC29437009B71E9 /* CacheEntry.swift */, ); path = Storage; sourceTree = ""; @@ -840,6 +845,7 @@ BDEDD36D1DBCE5D8007416A6 /* SyncCache.swift in Sources */, BDEDD36A1DBCE5D8007416A6 /* NSDate+Cache.swift in Sources */, BDEDD36F1DBCE5D8007416A6 /* DiskStorage.swift in Sources */, + 57506FC01EC2E1BA009B71E9 /* CacheEntry.swift in Sources */, D5A138C21EB29BFA00881A20 /* UIImage+Cache.swift in Sources */, BDEDD3601DBCE5CE007416A6 /* BasicHybridCache.swift in Sources */, BDEDD3701DBCE5D8007416A6 /* MemoryStorage.swift in Sources */, @@ -922,6 +928,7 @@ D5291D911C283CFB00B702C9 /* String+Cache.swift in Sources */, D5291D901C283CFB00B702C9 /* NSDate+Cache.swift in Sources */, D5A138C41EB29C2100881A20 /* NSImage+Cache.swift in Sources */, + 57506FBF1EC2E1B7009B71E9 /* CacheEntry.swift in Sources */, D5291D931C283CFB00B702C9 /* DiskStorage.swift in Sources */, D5291D861C283CFB00B702C9 /* BasicHybridCache.swift in Sources */, D5ACACCD1CD0207300567809 /* SyncHybridCache.swift in Sources */, @@ -960,6 +967,7 @@ D5291C321C28220B00B702C9 /* StorageKind.swift in Sources */, D5291C301C28220B00B702C9 /* Expiry.swift in Sources */, D5291C331C28220B00B702C9 /* JSON+Cache.swift in Sources */, + 57506FAE1EC29437009B71E9 /* CacheEntry.swift in Sources */, D5A138C11EB29BFA00881A20 /* UIImage+Cache.swift in Sources */, D5291C2F1C28220B00B702C9 /* Capsule.swift in Sources */, D5291C2D1C28220B00B702C9 /* Config.swift in Sources */, diff --git a/Cartfile.resolved b/Cartfile.resolved index 22c8326..e3f60a9 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,3 +1,3 @@ -github "krzyzanowskim/CryptoSwift" "0.6.8" -github "Quick/Nimble" "v6.1.0" +github "Quick/Nimble" "v7.0.0" github "Quick/Quick" "v1.1.0" +github "krzyzanowskim/CryptoSwift" "0.6.9" diff --git a/Source/Shared/BasicHybridCache.swift b/Source/Shared/BasicHybridCache.swift index fe646f0..34c5782 100644 --- a/Source/Shared/BasicHybridCache.swift +++ b/Source/Shared/BasicHybridCache.swift @@ -32,16 +32,22 @@ public class BasicHybridCache: NSObject { - Parameter name: A name of the cache - Parameter config: Cache configuration */ - public init(name: String, config: Config = Config.defaultConfig) { + public convenience init(name: String, config: Config = Config.defaultConfig) { + let frontStorage = StorageFactory.resolve(name, kind: config.frontKind, maxSize: UInt(config.maxObjects)) + let backStorage = StorageFactory.resolve(name, kind: config.backKind, maxSize: config.maxSize) + self.init(name: name, frontStorage: frontStorage, backStorage: backStorage, config: config) + + } + internal init(name: String, frontStorage: StorageAware, backStorage: StorageAware, config: Config) { self.name = name + self.frontStorage = frontStorage + self.backStorage = backStorage self.config = config - frontStorage = StorageFactory.resolve(name, kind: config.frontKind, maxSize: UInt(config.maxObjects)) - backStorage = StorageFactory.resolve(name, kind: config.backKind, maxSize: config.maxSize) super.init() - + let notificationCenter = NotificationCenter.default - + #if os(macOS) notificationCenter.addObserver(self, selector: #selector(clearExpiredDataInBackStorage), name: NSNotification.Name.NSApplicationWillTerminate, object: nil) @@ -56,7 +62,7 @@ public class BasicHybridCache: NSObject { name: .UIApplicationDidEnterBackground, object: nil) #endif } - + /** Removes notification center observer. */ @@ -96,20 +102,39 @@ public class BasicHybridCache: NSObject { - Parameter key: Unique key to identify the object in the cache - Parameter completion: Completion closure returns object or nil */ - func object(forKey key: String, completion: @escaping (_ object: T?) -> Void) { - frontStorage.object(key) { [weak self] (object: T?) in - if let object = object { - completion(object) + func object(forKey key: String, completion: @escaping (_ object: T?) -> Void){ + cacheEntry(forKey: key) { (entry: CacheEntry?) in + completion(entry?.object) + } + } + + /** + Tries to retrieve the cache entry from to the front and back cache storages. + + - Parameter key: Unique key to identify the cache entry in the cache + - Parameter completion: Completion closure returns cache entry or nil + */ + func cacheEntry(forKey key: String, completion: @escaping (_ object: CacheEntry?) -> Void) { + frontStorage.cacheEntry(key) { [weak self] (entry: CacheEntry?) in + if let entry = entry { + completion(entry) return } - + guard let weakSelf = self else { - completion(object) + completion(entry) return } - - weakSelf.backStorage.object(key) { (object: T?) in - completion(object) + + weakSelf.backStorage.cacheEntry(key) { (entry: CacheEntry?) in + guard let entry = entry else { + completion(nil) + return + } + + weakSelf.frontStorage.add(key, object: entry.object, expiry: entry.expiry) { _ in + completion(entry) + } } } } diff --git a/Source/Shared/Cache.swift b/Source/Shared/Cache.swift index 11da216..0c188c7 100644 --- a/Source/Shared/Cache.swift +++ b/Source/Shared/Cache.swift @@ -27,8 +27,17 @@ public final class Cache: BasicHybridCache { - Parameter key: Unique key to identify the object in the cache - Parameter completion: Completion closure returns object or nil */ - public func object(_ key: String, completion: @escaping (_ object: T?) -> Void) { super.object(forKey: key, completion: completion) } + + /** + Tries to retrieve the cache entry from to the front and back cache storages. + + - Parameter key: Unique key to identify the cache entry in the cache + - Parameter completion: Completion closure returns cache entry or nil + */ + public func cacheEntry(_ key: String, completion: @escaping (_ object: CacheEntry?) -> Void) { + super.cacheEntry(forKey: key, completion: completion) + } } diff --git a/Source/Shared/HybridCache.swift b/Source/Shared/HybridCache.swift index 77c78dd..2acedc3 100644 --- a/Source/Shared/HybridCache.swift +++ b/Source/Shared/HybridCache.swift @@ -26,4 +26,14 @@ public class HybridCache: BasicHybridCache { public func object(_ key: String, completion: @escaping (_ object: T?) -> Void) { super.object(forKey: key, completion: completion) } + + /** + Tries to retrieve the cache entry from to the front and back cache storages. + + - Parameter key: Unique key to identify the cache entry in the cache + - Parameter completion: Completion closure returns cache entry or nil + */ + public func cacheEntry(_ key: String, completion: @escaping (_ object: CacheEntry?) -> Void) { + super.cacheEntry(forKey: key, completion: completion) + } } diff --git a/Source/Shared/Storage/CacheEntry.swift b/Source/Shared/Storage/CacheEntry.swift new file mode 100644 index 0000000..0618a03 --- /dev/null +++ b/Source/Shared/Storage/CacheEntry.swift @@ -0,0 +1,4 @@ +public struct CacheEntry { + public let object: T + public let expiry: Expiry +} diff --git a/Source/Shared/Storage/DiskStorage.swift b/Source/Shared/Storage/DiskStorage.swift index 0b945af..e4f7a5f 100644 --- a/Source/Shared/Storage/DiskStorage.swift +++ b/Source/Shared/Storage/DiskStorage.swift @@ -87,30 +87,62 @@ public final class DiskStorage: StorageAware { completion?() } } - + /** - Tries to retrieve the object from the disk storage. - + Gets information about the cached object. + - Parameter key: Unique key to identify the object in the cache - Parameter completion: Completion closure returns object or nil */ public func object(_ key: String, completion: @escaping (_ object: T?) -> Void) { + cacheEntry(key) { (entry: CacheEntry?) in + completion(entry?.object) + } + } + + /** + Get cache entry which includes object with metadata. + + - Parameter key: Unique key to identify the object in the cache + - Parameter completion: Completion closure returns object wrapper with metadata or nil + */ + public func cacheEntry(_ key: String, completion: @escaping (_ object: CacheEntry?) -> Void) { readQueue.async { [weak self] in guard let weakSelf = self else { completion(nil) return } - + let filePath = weakSelf.filePath(key) var cachedObject: T? - + if let data = try? Data(contentsOf: URL(fileURLWithPath: filePath)) { cachedObject = T.decode(data) as? T } - - completion(cachedObject) + + if let cachedObject = cachedObject, + let expiry = weakSelf.cachedObjectExpiry(path: filePath) { + + completion(CacheEntry(object: cachedObject, expiry: expiry)) + return + } + + completion(nil) } } + + private func cachedObjectExpiry(path: String) -> Expiry? { + do { + let attributes = try fileManager.attributesOfItem(atPath: path) + + guard let modificationDate = attributes[FileAttributeKey.modificationDate] as? Date else { + return nil + } + return Expiry.date(modificationDate) + } catch {} + + return nil + } /** Removes the object from the cache by the given key. diff --git a/Source/Shared/Storage/MemoryStorage.swift b/Source/Shared/Storage/MemoryStorage.swift index e4adc25..29efb3b 100644 --- a/Source/Shared/Storage/MemoryStorage.swift +++ b/Source/Shared/Storage/MemoryStorage.swift @@ -64,26 +64,47 @@ public final class MemoryStorage: StorageAware { completion?() } } - + /** Tries to retrieve the object from the memory storage. - + - Parameter key: Unique key to identify the object in the cache - Parameter completion: Completion closure returns object or nil */ + public func object(_ key: String, completion: @escaping (_ object: T?) -> Void) { + cacheEntry(key) { (entry: CacheEntry?) in + completion(entry?.object) + } + } + + /** + Get cache entry which includes object with metadata. + + - Parameter key: Unique key to identify the object in the cache + - Parameter completion: Completion closure returns object wrapper with metadata or nil + */ + public func cacheEntry(_ key: String, completion: @escaping (_ object: CacheEntry?) -> Void) { readQueue.async { [weak self] in guard let weakSelf = self else { completion(nil) return } - - let capsule = weakSelf.cache.object(forKey: key as AnyObject) as? Capsule - completion(capsule?.object as? T) - - if let capsule = capsule { - weakSelf.removeIfExpired(key, capsule: capsule) + + guard let capsule = weakSelf.cache.object(forKey: key as AnyObject) as? Capsule else { + completion(nil) + return } + + var entry: CacheEntry? + + if let object = capsule.object as? T { + entry = CacheEntry(object: object, expiry: Expiry.date(capsule.expiryDate)) + } + + completion(entry) + + weakSelf.removeIfExpired(key, capsule: capsule) } } diff --git a/Source/Shared/Storage/StorageAware.swift b/Source/Shared/Storage/StorageAware.swift index a175e4e..79b8657 100644 --- a/Source/Shared/Storage/StorageAware.swift +++ b/Source/Shared/Storage/StorageAware.swift @@ -15,6 +15,14 @@ public protocol CacheAware { */ func add(_ key: String, object: T, expiry: Expiry, completion: (() -> Void)?) + /** + Get cache entry which includes object with metadata. + + - Parameter key: Unique key to identify the object in the cache + - Parameter completion: Completion closure returns object wrapper with metadata or nil + */ + func cacheEntry(_ key: String, completion: @escaping (_ object: CacheEntry?) -> Void) + /** Tries to retrieve the object from the cache. diff --git a/Tests/iOS/Specs/CacheSpec.swift b/Tests/iOS/Specs/CacheSpec.swift index 4a3ec45..793c899 100644 --- a/Tests/iOS/Specs/CacheSpec.swift +++ b/Tests/iOS/Specs/CacheSpec.swift @@ -71,11 +71,29 @@ class CacheSpec: QuickSpec { self.waitForExpectations(timeout: 8.0, handler:nil) } } - + + describe("#cacheEntry") { + it("resolves cache entry") { + waitUntil(timeout: 4.0) { done in + let expiryDate = Date() + + cache.add(key, object: object, expiry: .date(expiryDate)) { + cache.cacheEntry(key) { (entry: CacheEntry?) in + expect(entry?.object.firstName).to(equal(object.firstName)) + expect(entry?.object.lastName).to(equal(object.lastName)) + expect(entry?.expiry.date).to(equal(expiryDate)) + + done() + } + } + } + } + } + describe("#object") { it("resolves cached object") { let expectation = self.expectation(description: "Object Expectation") - + cache.add(key, object: object) { cache.object(key) { (receivedObject: User?) in expect(receivedObject?.firstName).to(equal(object.firstName)) @@ -83,9 +101,37 @@ class CacheSpec: QuickSpec { expectation.fulfill() } } - + self.waitForExpectations(timeout: 4.0, handler:nil) } + + it("should resolve from disk and set in-memory cache if object not in-memory") { + let frontStorage = MemoryStorage(name: "MemoryStorage") + let backStorage = DiskStorage(name: "DiskStorage") + let config = Config.defaultConfig + let key = "myusernamedjohn" + let object = SpecHelper.user + + let cache = Cache(name: "MyCache", frontStorage: frontStorage, backStorage: backStorage, config: config) + + waitUntil(timeout: 4.0) { done in + + backStorage.add(key, object: object) { + + cache.object(key) { (receivedObject: User?) in + + expect(receivedObject?.firstName).to(equal(object.firstName)) + expect(receivedObject?.lastName).to(equal(object.lastName)) + + frontStorage.object(key) { (inmemoryCachedUser: User?) in + expect(inmemoryCachedUser?.firstName).to(equal(object.firstName)) + expect(inmemoryCachedUser?.lastName).to(equal(object.lastName)) + done() + } + } + } + } + } } describe("#remove") { diff --git a/Tests/iOS/Specs/Storage/DiskStorageSpec.swift b/Tests/iOS/Specs/Storage/DiskStorageSpec.swift index a7bf78f..bcd3756 100644 --- a/Tests/iOS/Specs/Storage/DiskStorageSpec.swift +++ b/Tests/iOS/Specs/Storage/DiskStorageSpec.swift @@ -67,6 +67,39 @@ class DiskStorageSpec: QuickSpec { } } + describe("#cacheEntry") { + it("returns nil if entry doesn't exist") { + let storage = DiskStorage(name: name) + + waitUntil(timeout: 2.0) { done in + storage.cacheEntry(key) { (entry: CacheEntry?) in + expect(entry).to(beNil()) + done() + } + } + } + + it("returns entry if object exists") { + let storage = DiskStorage(name: name) + + waitUntil(timeout: 2.0) { done in + + storage.add(key, object: object) { + storage.cacheEntry(key) { (entry: CacheEntry?) in + + let attributes = try! fileManager.attributesOfItem(atPath: storage.filePath(key)) + let expiry = Expiry.date(attributes[FileAttributeKey.modificationDate] as! Date) + + expect(entry?.object.firstName).to(equal(object.firstName)) + expect(entry?.object.lastName).to(equal(object.lastName)) + expect(entry?.expiry.date).to(equal(expiry.date)) + done() + } + } + } + } + } + describe("#object") { it("resolves cached object") { let expectation = self.expectation(description: "Object Expectation") diff --git a/Tests/iOS/Specs/Storage/MemoryStorageSpec.swift b/Tests/iOS/Specs/Storage/MemoryStorageSpec.swift index 13d32f6..0952b4b 100644 --- a/Tests/iOS/Specs/Storage/MemoryStorageSpec.swift +++ b/Tests/iOS/Specs/Storage/MemoryStorageSpec.swift @@ -47,7 +47,38 @@ class MemoryStorageSpec: QuickSpec { self.waitForExpectations(timeout: 2.0, handler:nil) } } - + + describe("#cacheEntry") { + it("returns nil if entry doesn't exist") { + let storage = DiskStorage(name: name) + + waitUntil(timeout: 2.0) { done in + storage.cacheEntry(key) { (entry: CacheEntry?) in + expect(entry).to(beNil()) + done() + } + } + } + + it("returns entry if object exists") { + let storage = MemoryStorage(name: name) + let expiry = Expiry.date(Date()) + + waitUntil(timeout: 2.0) { done in + + storage.add(key, object: object, expiry: expiry) { + storage.cacheEntry(key) { (entry: CacheEntry?) in + + expect(entry?.object.firstName).to(equal(object.firstName)) + expect(entry?.object.lastName).to(equal(object.lastName)) + expect(entry?.expiry.date).to(equal(expiry.date)) + done() + } + } + } + } + } + describe("#object") { it("resolves cached object") { let expectation = self.expectation(description: "Object Expectation")