diff --git a/Cache.xcodeproj/project.pbxproj b/Cache.xcodeproj/project.pbxproj index 7e62ff5c..545b326d 100644 --- a/Cache.xcodeproj/project.pbxproj +++ b/Cache.xcodeproj/project.pbxproj @@ -7,9 +7,9 @@ objects = { /* Begin PBXBuildFile section */ - 57506FAE1EC29437009B71E9 /* ObjectMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */; }; - 57506FBF1EC2E1B7009B71E9 /* ObjectMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */; }; - 57506FC01EC2E1BA009B71E9 /* ObjectMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */; }; + 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 */; }; @@ -146,7 +146,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectMetadata.swift; sourceTree = ""; }; + 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 = ""; }; @@ -200,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 */ @@ -326,7 +326,7 @@ D5291C251C28220B00B702C9 /* MemoryStorage.swift */, D5291C261C28220B00B702C9 /* StorageAware.swift */, D5291C271C28220B00B702C9 /* StorageFactory.swift */, - 57506FAD1EC29437009B71E9 /* ObjectMetadata.swift */, + 57506FAD1EC29437009B71E9 /* CacheEntry.swift */, ); path = Storage; sourceTree = ""; @@ -845,7 +845,7 @@ BDEDD36D1DBCE5D8007416A6 /* SyncCache.swift in Sources */, BDEDD36A1DBCE5D8007416A6 /* NSDate+Cache.swift in Sources */, BDEDD36F1DBCE5D8007416A6 /* DiskStorage.swift in Sources */, - 57506FC01EC2E1BA009B71E9 /* ObjectMetadata.swift in Sources */, + 57506FC01EC2E1BA009B71E9 /* CacheEntry.swift in Sources */, D5A138C21EB29BFA00881A20 /* UIImage+Cache.swift in Sources */, BDEDD3601DBCE5CE007416A6 /* BasicHybridCache.swift in Sources */, BDEDD3701DBCE5D8007416A6 /* MemoryStorage.swift in Sources */, @@ -928,7 +928,7 @@ D5291D911C283CFB00B702C9 /* String+Cache.swift in Sources */, D5291D901C283CFB00B702C9 /* NSDate+Cache.swift in Sources */, D5A138C41EB29C2100881A20 /* NSImage+Cache.swift in Sources */, - 57506FBF1EC2E1B7009B71E9 /* ObjectMetadata.swift in Sources */, + 57506FBF1EC2E1B7009B71E9 /* CacheEntry.swift in Sources */, D5291D931C283CFB00B702C9 /* DiskStorage.swift in Sources */, D5291D861C283CFB00B702C9 /* BasicHybridCache.swift in Sources */, D5ACACCD1CD0207300567809 /* SyncHybridCache.swift in Sources */, @@ -967,7 +967,7 @@ D5291C321C28220B00B702C9 /* StorageKind.swift in Sources */, D5291C301C28220B00B702C9 /* Expiry.swift in Sources */, D5291C331C28220B00B702C9 /* JSON+Cache.swift in Sources */, - 57506FAE1EC29437009B71E9 /* ObjectMetadata.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/Source/Shared/BasicHybridCache.swift b/Source/Shared/BasicHybridCache.swift index ce181a76..173eb6e4 100644 --- a/Source/Shared/BasicHybridCache.swift +++ b/Source/Shared/BasicHybridCache.swift @@ -102,39 +102,42 @@ 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 - guard let object = object else { - completion(nil) - return + + 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) } - weakSelf.copyToFrontStorage(key, object: object, completion: completion) } } } - - private func copyToFrontStorage(_ key: String, object: T, completion: @escaping (_ object: T?) -> Void) { - - guard let metadata = self.backStorage.objectMetadata(key) else { - completion(nil) - return - } - - self.frontStorage.add(key, object: object, expiry: metadata.expiry, completion: { _ in - completion(object) - }) - } /** Removes the object from to the front and back cache storages. diff --git a/Source/Shared/Cache.swift b/Source/Shared/Cache.swift index 11da2162..0c188c76 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 77c78dd9..2acedc35 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 00000000..0618a037 --- /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 a1b65e3b..1396db2c 100644 --- a/Source/Shared/Storage/DiskStorage.swift +++ b/Source/Shared/Storage/DiskStorage.swift @@ -86,42 +86,57 @@ public final class DiskStorage: StorageAware { 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 objectMetadata(_ key: String) -> ObjectMetadata? { - - do { - let attributes = try fileManager.attributesOfItem(atPath: filePath(key)) - let fileModifiedDate = Expiry.date(attributes[FileAttributeKey.modificationDate] as! Date) - return ObjectMetadata(expiry: fileModifiedDate) - - } catch {} - - return nil + public func object(_ key: String, completion: @escaping (_ object: T?) -> Void) { + cacheEntry(key) { entry in + completion(entry?.object) + } } /** - Tries to retrieve the object from the disk storage. - + 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 or nil + - Parameter completion: Completion closure returns object wrapper with metadata or nil */ - public func object(_ key: String, completion: @escaping (_ object: T?) -> Void) { + 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 2d35003a..508f04a4 100644 --- a/Source/Shared/Storage/MemoryStorage.swift +++ b/Source/Shared/Storage/MemoryStorage.swift @@ -64,40 +64,47 @@ public final class MemoryStorage: StorageAware { completion?() } } - + /** - Gets information about the cached object. + 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 objectMetadata(_ key: String) -> ObjectMetadata? { - guard let capsule = cache.object(forKey: key as AnyObject) as? Capsule else { - return nil + public func object(_ key: String, completion: @escaping (_ object: T?) -> Void) { + cacheEntry(key) { entry in + completion(entry?.object) } - - return ObjectMetadata(expiry: Expiry.date(capsule.expiryDate)) } /** - Tries to retrieve the object from the memory storage. - + 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 or nil + - Parameter completion: Completion closure returns object wrapper with metadata or nil */ - public func object(_ key: String, completion: @escaping (_ object: T?) -> Void) { + 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/ObjectMetadata.swift b/Source/Shared/Storage/ObjectMetadata.swift deleted file mode 100644 index 5d866c3d..00000000 --- a/Source/Shared/Storage/ObjectMetadata.swift +++ /dev/null @@ -1,9 +0,0 @@ -public struct ObjectMetadata { - let expiry: Expiry -} - -extension ObjectMetadata: Equatable {} - -public func ==(lhs: ObjectMetadata, rhs: ObjectMetadata) -> Bool { - return lhs.expiry.date == rhs.expiry.date -} diff --git a/Source/Shared/Storage/StorageAware.swift b/Source/Shared/Storage/StorageAware.swift index c99164e5..44840df1 100644 --- a/Source/Shared/Storage/StorageAware.swift +++ b/Source/Shared/Storage/StorageAware.swift @@ -16,11 +16,12 @@ public protocol CacheAware { func add(_ key: String, object: T, expiry: Expiry, completion: (() -> Void)?) /** - Gets information about the cached 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 */ - func objectMetadata(_ key: String) -> ObjectMetadata? + 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 bcf589db..783fcfa5 100644 --- a/Tests/iOS/Specs/CacheSpec.swift +++ b/Tests/iOS/Specs/CacheSpec.swift @@ -65,11 +65,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)) @@ -77,36 +95,36 @@ 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) + 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 - 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)) - 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() - } - } + frontStorage.object(key) { (inmemoryCachedUser: User?) in + expect(inmemoryCachedUser?.firstName).to(equal(object.firstName)) + expect(inmemoryCachedUser?.lastName).to(equal(object.lastName)) + done() } + } } + } } } diff --git a/Tests/iOS/Specs/Storage/DiskStorageSpec.swift b/Tests/iOS/Specs/Storage/DiskStorageSpec.swift index 2fcca285..f1561340 100644 --- a/Tests/iOS/Specs/Storage/DiskStorageSpec.swift +++ b/Tests/iOS/Specs/Storage/DiskStorageSpec.swift @@ -65,28 +65,34 @@ class DiskStorageSpec: QuickSpec { } } - describe("#objectMetadata") { - it("returns nil if object doesn't exist") { + describe("#cacheEntry") { + it("returns nil if entry doesn't exist") { let storage = DiskStorage(name: name) - let metadata = storage.objectMetadata(key) - - expect(metadata).to(beNil()) + waitUntil(timeout: 2.0) { done in + storage.cacheEntry(key) { (entry: CacheEntry?) in + expect(entry).to(beNil()) + done() + } + } } - it("returns object metadata if object exists") { + it("returns entry if object exists") { let storage = DiskStorage(name: name) waitUntil(timeout: 2.0) { done in - + storage.add(key, object: object) { - let metadata = storage.objectMetadata(key) - let attributes = try! fileManager.attributesOfItem(atPath: storage.filePath(key)) - let fileModifiedDate = Expiry.date(attributes[FileAttributeKey.modificationDate] as! Date) - let expectedMetadata = ObjectMetadata(expiry: fileModifiedDate) - - expect(metadata).to(equal(expectedMetadata)) - done() + 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() + } } } } diff --git a/Tests/iOS/Specs/Storage/MemoryStorageSpec.swift b/Tests/iOS/Specs/Storage/MemoryStorageSpec.swift index aa5854c5..0952b4b4 100644 --- a/Tests/iOS/Specs/Storage/MemoryStorageSpec.swift +++ b/Tests/iOS/Specs/Storage/MemoryStorageSpec.swift @@ -48,26 +48,32 @@ class MemoryStorageSpec: QuickSpec { } } - describe("#objectMetadata") { - it("returns nil if object doesn't exist") { - let storage = MemoryStorage(name: name) - - let metadata = storage.objectMetadata(key) + describe("#cacheEntry") { + it("returns nil if entry doesn't exist") { + let storage = DiskStorage(name: name) - expect(metadata).to(beNil()) + waitUntil(timeout: 2.0) { done in + storage.cacheEntry(key) { (entry: CacheEntry?) in + expect(entry).to(beNil()) + done() + } + } } - it("returns object metadata if object exists") { + 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) { - let metadata = storage.objectMetadata(key) - let expectedMetadata = ObjectMetadata(expiry: expiry) - expect(metadata).to(equal(expectedMetadata)) - done() + 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() + } } } }