├── .swift-version
├── .mention-bot
├── Resources
├── CacheIcon.png
└── CachePresentation.png
├── Package.resolved
├── Playgrounds
├── Storage.playground
│ ├── contents.xcplayground
│ ├── playground.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── Contents.swift
└── SimpleStorage.playground
│ ├── contents.xcplayground
│ └── Contents.swift
├── Cache.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── Cache-iOS.xcscheme
│ ├── Cache-tvOS.xcscheme
│ └── Cache-macOS.xcscheme
├── Source
├── Shared
│ ├── Library
│ │ ├── Types.swift
│ │ ├── Optional+Extension.swift
│ │ ├── ObservationToken.swift
│ │ ├── TypeWrapper.swift
│ │ ├── Transformer.swift
│ │ ├── Result.swift
│ │ ├── Entry.swift
│ │ ├── MemoryCapsule.swift
│ │ ├── StorageError.swift
│ │ ├── ExpirationMode.swift
│ │ ├── DataSerializer.swift
│ │ ├── ImageWrapper.swift
│ │ ├── Expiry.swift
│ │ ├── JSONArrayWrapper.swift
│ │ ├── JSONDictionaryWrapper.swift
│ │ ├── TransformerFactory.swift
│ │ └── MD5.swift
│ ├── Extensions
│ │ ├── Date+Extensions.swift
│ │ └── JSONDecoder+Extensions.swift
│ ├── Storage
│ │ ├── Storage+Transform.swift
│ │ ├── StorageObservationRegistry.swift
│ │ ├── KeyObservationRegistry.swift
│ │ ├── SyncStorage.swift
│ │ ├── MemoryStorage.swift
│ │ ├── StorageAware.swift
│ │ ├── AsyncStorage.swift
│ │ ├── Storage.swift
│ │ ├── HybridStorage.swift
│ │ └── DiskStorage.swift
│ └── Configuration
│ │ ├── MemoryConfig.swift
│ │ └── DiskConfig.swift
├── iOS
│ └── UIImage+Extensions.swift
└── Mac
│ └── NSImage+Extensions.swift
├── Tests
├── iOS
│ ├── Tests
│ │ ├── Library
│ │ │ ├── ObservationTokenTests.swift
│ │ │ ├── ImageWrapperTests.swift
│ │ │ ├── MemoryCapsuleTests.swift
│ │ │ ├── ExpiryTests.swift
│ │ │ ├── JSONWrapperTests.swift
│ │ │ ├── ObjectConverterTests.swift
│ │ │ ├── TypeWrapperTests.swift
│ │ │ └── MD5Tests.swift
│ │ ├── Extensions
│ │ │ └── Date+ExtensionsTests.swift
│ │ └── Storage
│ │ │ ├── SyncStorageTests.swift
│ │ │ ├── AsyncStorageTests.swift
│ │ │ ├── MemoryStorageTests.swift
│ │ │ ├── StorageSupportTests.swift
│ │ │ ├── DiskStorageTests.swift
│ │ │ ├── StorageTests.swift
│ │ │ └── HybridStorageTests.swift
│ ├── Helpers
│ │ ├── TestHelper+iOS.swift
│ │ └── UIImage+ExtensionsTests.swift
│ └── Info.plist
├── Mac
│ ├── Helpers
│ │ ├── TestHelper+OSX.swift
│ │ └── NSImage+ExtensionsTests.swift
│ └── Info.plist
├── Shared
│ ├── TestCase+Extensions.swift
│ ├── User.swift
│ └── TestHelper.swift
└── tvOS
│ └── Info.plist
├── .gitignore
├── CONTRIBUTING.md
├── Package.swift
├── SupportFiles
├── tvOS
│ └── Info.plist
├── iOS
│ └── Info.plist
└── Mac
│ └── Info.plist
├── Cache.podspec
├── .circleci
└── config.yml
├── LICENSE.md
├── .swiftlint.yml
├── CHANGELOG.md
└── README.md
/.swift-version:
--------------------------------------------------------------------------------
1 | 5.0
2 |
--------------------------------------------------------------------------------
/.mention-bot:
--------------------------------------------------------------------------------
1 | {
2 | "maxReviewers": 2
3 | }
4 |
--------------------------------------------------------------------------------
/Resources/CacheIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/Cache/master/Resources/CacheIcon.png
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 |
5 | ]
6 | },
7 | "version": 1
8 | }
9 |
--------------------------------------------------------------------------------
/Resources/CachePresentation.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ml-archive/Cache/master/Resources/CachePresentation.png
--------------------------------------------------------------------------------
/Playgrounds/Storage.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Cache.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Playgrounds/SimpleStorage.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/Playgrounds/Storage.playground/playground.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Source/Shared/Library/Types.swift:
--------------------------------------------------------------------------------
1 | #if os(iOS) || os(tvOS)
2 | import UIKit
3 | public typealias Image = UIImage
4 | #elseif os(watchOS)
5 |
6 | #elseif os(OSX)
7 | import AppKit
8 | public typealias Image = NSImage
9 | #endif
10 |
--------------------------------------------------------------------------------
/Source/Shared/Extensions/Date+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /**
4 | Helper NSDate extension.
5 | */
6 | extension Date {
7 |
8 | /// Checks if the date is in the past.
9 | var inThePast: Bool {
10 | return timeIntervalSinceNow < 0
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Source/Shared/Library/Optional+Extension.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension Optional {
4 | func unwrapOrThrow(error: Error) throws -> Wrapped {
5 | if let value = self {
6 | return value
7 | } else {
8 | throw error
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Cache.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Source/Shared/Library/ObservationToken.swift:
--------------------------------------------------------------------------------
1 | public final class ObservationToken {
2 | private let cancellationClosure: () -> Void
3 |
4 | init(cancellationClosure: @escaping () -> Void) {
5 | self.cancellationClosure = cancellationClosure
6 | }
7 |
8 | public func cancel() {
9 | cancellationClosure()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Source/Shared/Library/TypeWrapper.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Used to wrap Codable object
4 | public struct TypeWrapper: Codable {
5 | enum CodingKeys: String, CodingKey {
6 | case object
7 | }
8 |
9 | public let object: T
10 |
11 | public init(object: T) {
12 | self.object = object
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Source/Shared/Library/Transformer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class Transformer {
4 | let toData: (T) throws -> Data
5 | let fromData: (Data) throws -> T
6 |
7 | public init(toData: @escaping (T) throws -> Data, fromData: @escaping (Data) throws -> T) {
8 | self.toData = toData
9 | self.fromData = fromData
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Library/ObservationTokenTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class ObservationTokenTests: XCTestCase {
5 | func testCancel() {
6 | var cancelled = false
7 |
8 | let token = ObservationToken {
9 | cancelled = true
10 | }
11 |
12 | token.cancel()
13 | XCTAssertTrue(cancelled)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/Mac/Helpers/TestHelper+OSX.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 |
3 | extension TestHelper {
4 | static func image(color: NSColor = .red, size: NSSize = .init(width: 1, height: 1)) -> NSImage {
5 | let image = NSImage(size: size)
6 | image.lockFocus()
7 | color.drawSwatch(in: NSMakeRect(0, 0, size.width, size.height))
8 | image.unlockFocus()
9 | return image
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Extensions/Date+ExtensionsTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class DateCacheTests: XCTestCase {
5 | func testInThePast() {
6 | var date = Date(timeInterval: 100000, since: Date())
7 | XCTAssertFalse(date.inThePast)
8 |
9 | date = Date(timeInterval: -100000, since: Date())
10 | XCTAssertTrue(date.inThePast)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Tests/Shared/TestCase+Extensions.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | extension XCTestCase {
4 | func given(_ description: String, closure: () throws -> Void) rethrows {
5 | try closure()
6 | }
7 |
8 | func when(_ description: String, closure: () throws -> Void) rethrows {
9 | try closure()
10 | }
11 |
12 | func then(_ description: String, closure: () throws -> Void) rethrows {
13 | try closure()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Tests/Shared/User.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import Cache
3 |
4 | struct User: Codable, Equatable {
5 | let firstName: String
6 | let lastName: String
7 |
8 | enum CodingKeys: String, CodingKey {
9 | case firstName = "first_name"
10 | case lastName = "last_name"
11 | }
12 | }
13 |
14 | func == (lhs:User, rhs: User) -> Bool {
15 | return lhs.firstName == rhs.firstName
16 | && lhs.lastName == rhs.lastName
17 | }
18 |
--------------------------------------------------------------------------------
/Source/Shared/Library/Result.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Used for callback in async operations.
4 | public enum Result {
5 | case value(T)
6 | case error(Error)
7 |
8 | public func map(_ transform: (T) -> U) -> Result {
9 | switch self {
10 | case .value(let value):
11 | return Result.value(transform(value))
12 | case .error(let error):
13 | return Result.error(error)
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/Mac/Helpers/NSImage+ExtensionsTests.swift:
--------------------------------------------------------------------------------
1 | import Cocoa
2 | @testable import Cache
3 |
4 | extension NSImage {
5 | func isEqualToImage(_ image: NSImage) -> Bool {
6 | return data == image.data
7 | }
8 |
9 | var data: Data {
10 | let representation = tiffRepresentation!
11 | let imageFileType: NSBitmapImageRep.FileType = .png
12 |
13 | return NSBitmapImageRep(data: representation)!
14 | .representation(using: imageFileType, properties: [:])!
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 | .AppleDouble
4 | .LSOverride
5 | Icon
6 | ._*
7 | .Spotlight-V100
8 | .Trashes
9 |
10 | # Xcode
11 | #
12 | build/
13 | *.pbxuser
14 | !default.pbxuser
15 | *.mode1v3
16 | !default.mode1v3
17 | *.mode2v3
18 | !default.mode2v3
19 | *.perspectivev3
20 | !default.perspectivev3
21 | xcuserdata
22 | *.xccheckout
23 | *.moved-aside
24 | DerivedData
25 | *.hmap
26 | *.ipa
27 | *.xcuserstate
28 |
29 | # CocoaPods
30 | Pods
31 |
32 | # Carthage
33 | Carthage
34 | /.build
35 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | GitHub Issues is for reporting bugs, discussing features and general feedback in **Cache**. Be sure to check our [documentation](http://cocoadocs.org/docsets/Cache), [FAQ](https://github.com/hyperoslo/Cache/wiki/FAQ) and [past issues](https://github.com/hyperoslo/Cache/issues?state=closed) before opening any new issues.
2 |
3 | If you are posting about a crash in your application, a stack trace is helpful, but additional context, in the form of code and explanation, is necessary to be of any use.
4 |
--------------------------------------------------------------------------------
/Source/Shared/Library/Entry.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A wrapper around cached object and its expiry date.
4 | public struct Entry {
5 | /// Cached object
6 | public let object: T
7 | /// Expiry date
8 | public let expiry: Expiry
9 | /// File path to the cached object
10 | public let filePath: String?
11 |
12 | init(object: T, expiry: Expiry, filePath: String? = nil) {
13 | self.object = object
14 | self.expiry = expiry
15 | self.filePath = filePath
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Library/ImageWrapperTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class ImageWrapperTests: XCTestCase {
5 | func testImage() {
6 | let image = TestHelper.image(size: CGSize(width: 100, height: 100))
7 | let wrapper = ImageWrapper(image: image)
8 |
9 | let data = try! JSONEncoder().encode(wrapper)
10 | let anotherWrapper = try! JSONDecoder().decode(ImageWrapper.self, from: data)
11 |
12 | XCTAssertTrue(image.isEqualToImage(anotherWrapper.image))
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Tests/iOS/Helpers/TestHelper+iOS.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension TestHelper {
4 | static func image(_ color: UIColor = .red, size: CGSize = .init(width: 1, height: 1)) -> UIImage {
5 | UIGraphicsBeginImageContextWithOptions(size, false, 0)
6 |
7 | let context = UIGraphicsGetCurrentContext()
8 | context?.setFillColor(color.cgColor)
9 | context?.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))
10 |
11 | let image = UIGraphicsGetImageFromCurrentImageContext()
12 | UIGraphicsEndImageContext()
13 |
14 | return image!
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Source/Shared/Library/MemoryCapsule.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Helper class to hold cached instance and expiry date.
4 | /// Used in memory storage to work with NSCache.
5 | class MemoryCapsule: NSObject {
6 | /// Object to be cached
7 | let object: Any
8 | /// Expiration date
9 | let expiry: Expiry
10 |
11 | /**
12 | Creates a new instance of Capsule.
13 | - Parameter value: Object to be cached
14 | - Parameter expiry: Expiration date
15 | */
16 | init(value: Any, expiry: Expiry) {
17 | self.object = value
18 | self.expiry = expiry
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Source/Shared/Library/StorageError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum StorageError: Error {
4 | /// Object can not be found
5 | case notFound
6 | /// Object is found, but casting to requested type failed
7 | case typeNotMatch
8 | /// The file attributes are malformed
9 | case malformedFileAttributes
10 | /// Can't perform Decode
11 | case decodingFailed
12 | /// Can't perform Encode
13 | case encodingFailed
14 | /// The storage has been deallocated
15 | case deallocated
16 | /// Fail to perform transformation to or from Data
17 | case transformerFail
18 | }
19 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/Storage+Transform.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension Storage {
4 | func transformData() -> Storage {
5 | let storage = transform(transformer: TransformerFactory.forData())
6 | return storage
7 | }
8 |
9 | func transformImage() -> Storage {
10 | let storage = transform(transformer: TransformerFactory.forImage())
11 | return storage
12 | }
13 |
14 | func transformCodable(ofType: U.Type) -> Storage {
15 | let storage = transform(transformer: TransformerFactory.forCodable(ofType: U.self))
16 | return storage
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "Cache",
7 | products: [
8 | .library(
9 | name: "Cache",
10 | targets: ["Cache"]),
11 | ],
12 | dependencies: [],
13 | targets: [
14 | .target(
15 | name: "Cache",
16 | path: "Source/Shared",
17 | exclude: ["Library/ImageWrapper.swift"]), // relative to the target path
18 | .testTarget(
19 | name: "CacheTests",
20 | dependencies: ["Cache"],
21 | path: "Tests"),
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/Source/Shared/Library/ExpirationMode.swift:
--------------------------------------------------------------------------------
1 | /// Sets the expiration mode for the `CacheManager`. The default value is `.auto` which means that `Cache`
2 | /// will handle expiration internally. It will trigger cache clean up tasks depending on the events its receives
3 | /// from the application. If expiration mode is set to manual, it means that you manually have to invoke the clear
4 | /// cache methods yourself.
5 | ///
6 | /// - auto: Automatic cleanup of expired objects (default).
7 | /// - manual: Manual means that you opt out from any automatic expiration handling.
8 | public enum ExpirationMode {
9 | case auto, manual
10 | }
11 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Library/MemoryCapsuleTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class MemoryCapsuleTests: XCTestCase {
5 | let testObject = User(firstName: "a", lastName: "b")
6 |
7 | func testExpiredWhenNotExpired() {
8 | let date = Date(timeInterval: 100000, since: Date())
9 | let capsule = MemoryCapsule(value: testObject, expiry: .date(date))
10 |
11 | XCTAssertFalse(capsule.expiry.isExpired)
12 | }
13 |
14 | func testExpiredWhenExpired() {
15 | let date = Date(timeInterval: -100000, since: Date())
16 | let capsule = MemoryCapsule(value: testObject, expiry: .date(date))
17 |
18 | XCTAssertTrue(capsule.expiry.isExpired)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Tests/iOS/Helpers/UIImage+ExtensionsTests.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIImage {
4 | func isEqualToImage(_ image: UIImage) -> Bool {
5 | let data = normalizedData()
6 | return data == image.normalizedData()
7 | }
8 |
9 | func normalizedData() -> Data {
10 | let pixelSize = CGSize(
11 | width : size.width * scale,
12 | height : size.height * scale
13 | )
14 |
15 | UIGraphicsBeginImageContext(pixelSize)
16 | draw(
17 | in: CGRect(x: 0, y: 0, width: pixelSize.width,
18 | height: pixelSize.height)
19 | )
20 |
21 | let drawnImage = UIGraphicsGetImageFromCurrentImageContext()
22 | UIGraphicsEndImageContext()
23 |
24 | return drawnImage!.cgImage!.dataProvider!.data! as Data
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/tvOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Source/Shared/Library/DataSerializer.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Convert to and from data
4 | class DataSerializer {
5 |
6 | /// Convert object to data
7 | ///
8 | /// - Parameter object: The object to convert
9 | /// - Returns: Data
10 | /// - Throws: Encoder error if any
11 | static func serialize(object: T) throws -> Data {
12 | let encoder = JSONEncoder()
13 | return try encoder.encode(object)
14 | }
15 |
16 | /// Convert data to object
17 | ///
18 | /// - Parameter data: The data to convert
19 | /// - Returns: The object
20 | /// - Throws: Decoder error if any
21 | static func deserialize(data: Data) throws -> T {
22 | let decoder = JSONDecoder()
23 | return try decoder.decode(T.self, from: data)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Source/iOS/UIImage+Extensions.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | /// Helper UIImage extension.
4 | extension UIImage {
5 | /// Checks if image has alpha component
6 | var hasAlpha: Bool {
7 | let result: Bool
8 |
9 | guard let alpha = cgImage?.alphaInfo else {
10 | return false
11 | }
12 |
13 | switch alpha {
14 | case .none, .noneSkipFirst, .noneSkipLast:
15 | result = false
16 | default:
17 | result = true
18 | }
19 |
20 | return result
21 | }
22 |
23 | /// Convert to data
24 | func cache_toData() -> Data? {
25 | #if swift(>=4.2)
26 | return hasAlpha
27 | ? pngData()
28 | : jpegData(compressionQuality: 1.0)
29 | #else
30 | return hasAlpha
31 | ? UIImagePNGRepresentation(self)
32 | : UIImageJPEGRepresentation(self, 1.0)
33 | #endif
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/Mac/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Tests/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/SupportFiles/tvOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Source/Shared/Configuration/MemoryConfig.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct MemoryConfig {
4 | /// Expiry date that will be applied by default for every added object
5 | /// if it's not overridden in the add(key: object: expiry: completion:) method
6 | public let expiry: Expiry
7 | /// The maximum number of objects in memory the cache should hold.
8 | /// If 0, there is no count limit. The default value is 0.
9 | public let countLimit: UInt
10 |
11 | /// The maximum total cost that the cache can hold before it starts evicting objects.
12 | /// If 0, there is no total cost limit. The default value is 0
13 | public let totalCostLimit: UInt
14 |
15 | public init(expiry: Expiry = .never, countLimit: UInt = 0, totalCostLimit: UInt = 0) {
16 | self.expiry = expiry
17 | self.countLimit = countLimit
18 | self.totalCostLimit = totalCostLimit
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Cache.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "Cache"
3 | s.summary = "Nothing but cache."
4 | s.version = "5.2.0"
5 | s.homepage = "https://github.com/hyperoslo/Cache"
6 | s.license = 'MIT'
7 | s.author = { "Hyper Interaktiv AS" => "ios@hyper.no" }
8 | s.source = { :git => "https://github.com/hyperoslo/Cache.git", :tag => s.version.to_s }
9 | s.social_media_url = 'https://twitter.com/hyperoslo'
10 |
11 | s.ios.deployment_target = '8.0'
12 | s.osx.deployment_target = '10.9'
13 | s.tvos.deployment_target = '9.2'
14 |
15 | s.requires_arc = true
16 | s.ios.source_files = 'Source/{iOS,Shared}/**/*'
17 | s.osx.source_files = 'Source/{Mac,Shared}/**/*'
18 | s.tvos.source_files = 'Source/{iOS,Shared}/**/*'
19 |
20 | s.frameworks = 'Foundation'
21 |
22 | s.pod_target_xcconfig = { 'SWIFT_VERSION' => '4.2' }
23 | end
24 |
--------------------------------------------------------------------------------
/SupportFiles/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Source/Shared/Library/ImageWrapper.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct ImageWrapper: Codable {
4 | public let image: Image
5 |
6 | public enum CodingKeys: String, CodingKey {
7 | case image
8 | }
9 |
10 | public init(image: Image) {
11 | self.image = image
12 | }
13 |
14 | public init(from decoder: Decoder) throws {
15 | let container = try decoder.container(keyedBy: CodingKeys.self)
16 | let data = try container.decode(Data.self, forKey: CodingKeys.image)
17 | guard let image = Image(data: data) else {
18 | throw StorageError.decodingFailed
19 | }
20 |
21 | self.image = image
22 | }
23 |
24 | public func encode(to encoder: Encoder) throws {
25 | var container = encoder.container(keyedBy: CodingKeys.self)
26 | guard let data = image.cache_toData() else {
27 | throw StorageError.encodingFailed
28 | }
29 |
30 | try container.encode(data, forKey: CodingKeys.image)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Library/ExpiryTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class ExpiryTests: XCTestCase {
5 | /// Tests that it returns date in the distant future
6 | func testNever() {
7 | let date = Date(timeIntervalSince1970: 60 * 60 * 24 * 365 * 68)
8 | let expiry = Expiry.never
9 |
10 | XCTAssertEqual(expiry.date, date)
11 | }
12 |
13 | /// Tests that it returns date by adding time interval
14 | func testSeconds() {
15 | let date = Date().addingTimeInterval(1000)
16 | let expiry = Expiry.seconds(1000)
17 |
18 | XCTAssertEqual(
19 | expiry.date.timeIntervalSinceReferenceDate,
20 | date.timeIntervalSinceReferenceDate,
21 | accuracy: 0.1
22 | )
23 | }
24 |
25 | /// Tests that it returns a specified date
26 | func testDate() {
27 | let date = Date().addingTimeInterval(1000)
28 | let expiry = Expiry.date(date)
29 |
30 | XCTAssertEqual(expiry.date, date)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Source/Shared/Library/Expiry.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /**
4 | Helper enum to set the expiration date
5 | */
6 | public enum Expiry {
7 | /// Object will be expired in the nearest future
8 | case never
9 | /// Object will be expired in the specified amount of seconds
10 | case seconds(TimeInterval)
11 | /// Object will be expired on the specified date
12 | case date(Date)
13 |
14 | /// Returns the appropriate date object
15 | public var date: Date {
16 | switch self {
17 | case .never:
18 | // Ref: http://lists.apple.com/archives/cocoa-dev/2005/Apr/msg01833.html
19 | return Date(timeIntervalSince1970: 60 * 60 * 24 * 365 * 68)
20 | case .seconds(let seconds):
21 | return Date().addingTimeInterval(seconds)
22 | case .date(let date):
23 | return date
24 | }
25 | }
26 |
27 | /// Checks if cached object is expired according to expiration date
28 | public var isExpired: Bool {
29 | return date.inThePast
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Source/Mac/NSImage+Extensions.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 |
3 | /// Helper UIImage extension.
4 | extension NSImage {
5 | /// Checks if image has alpha component
6 | var hasAlpha: Bool {
7 | var imageRect: CGRect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
8 |
9 | guard let imageRef = cgImage(forProposedRect: &imageRect, context: nil, hints: nil) else {
10 | return false
11 | }
12 |
13 | let result: Bool
14 | let alpha = imageRef.alphaInfo
15 |
16 | switch alpha {
17 | case .none, .noneSkipFirst, .noneSkipLast:
18 | result = false
19 | default:
20 | result = true
21 | }
22 |
23 | return result
24 | }
25 |
26 | /// Convert to data
27 | func cache_toData() -> Data? {
28 | guard let data = tiffRepresentation else {
29 | return nil
30 | }
31 |
32 | let imageFileType: NSBitmapImageRep.FileType = hasAlpha ? .png : .jpeg
33 | return NSBitmapImageRep(data: data)?
34 | .representation(using: imageFileType, properties: [:])
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/SupportFiles/Mac/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSHumanReadableCopyright
24 | Copyright © 2015 Hyper Interaktiv AS. All rights reserved.
25 | NSPrincipalClass
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build-and-test:
4 | macos:
5 | xcode: "10.0.0"
6 | shell: /bin/bash --login -o pipefail
7 | steps:
8 | - checkout
9 | - run: xcodebuild -project Cache.xcodeproj -scheme "Cache-macOS" -sdk macosx clean
10 | - run: xcodebuild -project Cache.xcodeproj -scheme "Cache-macOS" -sdk macosx -enableCodeCoverage YES test
11 | - run: xcodebuild -project Cache.xcodeproj -scheme "Cache-iOS" -sdk iphonesimulator clean
12 | - run: xcodebuild -project Cache.xcodeproj -scheme "Cache-iOS" -sdk iphonesimulator -destination 'platform=iOS Simulator,OS=12.0,name=iPhone X' -enableCodeCoverage YES test
13 | - run: xcodebuild -project Cache.xcodeproj -scheme "Cache-tvOS" -destination 'platform=tvOS Simulator,name=Apple TV,OS=12.0' clean
14 | - run: xcodebuild -project Cache.xcodeproj -scheme "Cache-tvOS" -destination 'platform=tvOS Simulator,name=Apple TV,OS=12.0' -enableCodeCoverage YES test
15 |
16 | workflows:
17 | version: 2
18 | build-and-test:
19 | jobs:
20 | - build-and-test
21 |
--------------------------------------------------------------------------------
/Tests/Shared/TestHelper.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | #if os(iOS) || os(tvOS)
4 | import UIKit
5 | #elseif os(OSX)
6 | import AppKit
7 | #endif
8 |
9 | struct TestHelper {
10 | static func data(_ length : Int) -> Data {
11 | let buffer = [UInt8](repeating: 0, count: length)
12 | return Data(bytes: buffer)
13 | }
14 |
15 | static func triggerApplicationEvents() {
16 | #if (iOS)
17 | NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
18 | NotificationCenter.default.post(name: UIApplication.willTerminateNotification, object: nil)
19 | #elseif os(tvOS)
20 | NotificationCenter.default.post(name: Notification.Name.UIApplicationDidEnterBackground, object: nil)
21 | NotificationCenter.default.post(name: Notification.Name.UIApplicationWillTerminate, object: nil)
22 | #else
23 | NotificationCenter.default.post(name: NSApplication.willTerminateNotification, object: nil)
24 | NotificationCenter.default.post(name: NSApplication.didResignActiveNotification, object: nil)
25 | #endif
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Source/Shared/Library/JSONArrayWrapper.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public typealias JSONArray = [JSONDictionary]
4 |
5 | public struct JSONArrayWrapper: Codable {
6 | public let jsonArray: JSONArray
7 |
8 | public enum CodingKeys: String, CodingKey {
9 | case jsonArray
10 | }
11 |
12 | public init(jsonArray: JSONArray) {
13 | self.jsonArray = jsonArray
14 | }
15 |
16 | public init(from decoder: Decoder) throws {
17 | let container = try decoder.container(keyedBy: CodingKeys.self)
18 | let data = try container.decode(Data.self, forKey: CodingKeys.jsonArray)
19 | let object = try JSONSerialization.jsonObject(
20 | with: data,
21 | options: []
22 | )
23 |
24 | guard let jsonArray = object as? JSONArray else {
25 | throw StorageError.decodingFailed
26 | }
27 |
28 | self.jsonArray = jsonArray
29 | }
30 |
31 | public func encode(to encoder: Encoder) throws {
32 | var container = encoder.container(keyedBy: CodingKeys.self)
33 | let data = try JSONSerialization.data(
34 | withJSONObject: jsonArray,
35 | options: []
36 | )
37 |
38 | try container.encode(data, forKey: CodingKeys.jsonArray)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Licensed under the **MIT** license
2 |
3 | > Copyright (c) 2015 Hyper Interaktiv AS
4 | >
5 | > Permission is hereby granted, free of charge, to any person obtaining
6 | > a copy of this software and associated documentation files (the
7 | > "Software"), to deal in the Software without restriction, including
8 | > without limitation the rights to use, copy, modify, merge, publish,
9 | > distribute, sublicense, and/or sell copies of the Software, and to
10 | > permit persons to whom the Software is furnished to do so, subject to
11 | > the following conditions:
12 | >
13 | > The above copyright notice and this permission notice shall be
14 | > included in all copies or substantial portions of the Software.
15 | >
16 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Source/Shared/Library/JSONDictionaryWrapper.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public typealias JSONDictionary = [String: Any]
4 |
5 | public struct JSONDictionaryWrapper: Codable {
6 | public let jsonDictionary: JSONDictionary
7 |
8 | public enum CodingKeys: String, CodingKey {
9 | case jsonDictionary
10 | }
11 |
12 | public init(jsonDictionary: JSONDictionary) {
13 | self.jsonDictionary = jsonDictionary
14 | }
15 |
16 | public init(from decoder: Decoder) throws {
17 | let container = try decoder.container(keyedBy: CodingKeys.self)
18 | let data = try container.decode(Data.self, forKey: CodingKeys.jsonDictionary)
19 | let object = try JSONSerialization.jsonObject(
20 | with: data,
21 | options: []
22 | )
23 |
24 | guard let jsonDictionary = object as? JSONDictionary else {
25 | throw StorageError.decodingFailed
26 | }
27 |
28 | self.jsonDictionary = jsonDictionary
29 | }
30 |
31 | public func encode(to encoder: Encoder) throws {
32 | var container = encoder.container(keyedBy: CodingKeys.self)
33 | let data = try JSONSerialization.data(
34 | withJSONObject: jsonDictionary,
35 | options: []
36 | )
37 |
38 | try container.encode(data, forKey: CodingKeys.jsonDictionary)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Library/JSONWrapperTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import Cache
3 |
4 | final class JSONWrapperTests: XCTestCase {
5 | func testJSONDictionary() {
6 | let json: JSONDictionary = [
7 | "name": "John Snow",
8 | "location": "Winterfell"
9 | ]
10 |
11 | let wrapper = JSONDictionaryWrapper(jsonDictionary: json)
12 |
13 | let data = try! JSONEncoder().encode(wrapper)
14 | let decodedWrapper = try! JSONDecoder().decode(JSONDictionaryWrapper.self, from: data)
15 |
16 | XCTAssertEqual(
17 | NSDictionary(dictionary: decodedWrapper.jsonDictionary),
18 | NSDictionary(dictionary: json)
19 | )
20 | }
21 |
22 | func testJSONArray() {
23 | let json: JSONArray = [
24 | [
25 | "name": "John Snow",
26 | "location": "Winterfell"
27 | ],
28 | [
29 | "name": "Daenerys Targaryen",
30 | "location": "Dragonstone"
31 | ]
32 | ]
33 |
34 | let wrapper = JSONArrayWrapper(jsonArray: json)
35 |
36 | let data = try! JSONEncoder().encode(wrapper)
37 | let decodedWrapper = try! JSONDecoder().decode(JSONArrayWrapper.self, from: data)
38 |
39 | zip(json, decodedWrapper.jsonArray).forEach {
40 | XCTAssertEqual(
41 | NSDictionary(dictionary: $0),
42 | NSDictionary(dictionary: $1)
43 | )
44 | }
45 | }
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/Source/Shared/Library/TransformerFactory.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class TransformerFactory {
4 | public static func forData() -> Transformer {
5 | let toData: (Data) throws -> Data = { $0 }
6 |
7 | let fromData: (Data) throws -> Data = { $0 }
8 |
9 | return Transformer(toData: toData, fromData: fromData)
10 | }
11 |
12 | public static func forImage() -> Transformer {
13 | let toData: (Image) throws -> Data = { image in
14 | return try image.cache_toData().unwrapOrThrow(error: StorageError.transformerFail)
15 | }
16 |
17 | let fromData: (Data) throws -> Image = { data in
18 | return try Image(data: data).unwrapOrThrow(error: StorageError.transformerFail)
19 | }
20 |
21 | return Transformer(toData: toData, fromData: fromData)
22 | }
23 |
24 | public static func forCodable(ofType: U.Type) -> Transformer {
25 | let toData: (U) throws -> Data = { object in
26 | let wrapper = TypeWrapper(object: object)
27 | let encoder = JSONEncoder()
28 | return try encoder.encode(wrapper)
29 | }
30 |
31 | let fromData: (Data) throws -> U = { data in
32 | let decoder = JSONDecoder()
33 | return try decoder.decode(TypeWrapper.self, from: data).object
34 | }
35 |
36 | return Transformer(toData: toData, fromData: fromData)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | included: # paths to include during linting. `--path` is ignored if present.
2 | - Source
3 | excluded: # paths to ignore during linting. Takes precedence over `included`.
4 | - Carthage
5 | - Pods
6 | disabled_rules:
7 | - type_name
8 | - vertical_parameter_alignment
9 |
10 | # configurable rules can be customized from this configuration file
11 | # binary rules can set their severity level
12 | force_cast: warning # implicitly
13 | force_try:
14 | severity: warning # explicitly
15 | # rules that have both warning and error levels, can set just the warning level
16 | # implicitly
17 | line_length: 200
18 | # they can set both implicitly with an array
19 | type_body_length:
20 | - 300 # warning
21 | - 400 # error
22 | # or they can set both explicitly
23 | file_length:
24 | warning: 500
25 | error: 1200
26 | # naming rules can set warnings/errors for min_length and max_length
27 | # additionally they can set excluded names
28 | type_name:
29 | min_length: 3 # only warning
30 | max_length: # warning and error
31 | warning: 40
32 | error: 50
33 | excluded: iPhone # excluded via string
34 | variable_name:
35 | min_length: # only min_length
36 | error: 2 # only error
37 | excluded: # excluded via string array
38 | - x
39 | - y
40 | - id
41 | - URL
42 | - GlobalAPIKey
43 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle)
44 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/StorageObservationRegistry.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A protocol used for adding and removing storage observations
4 | public protocol StorageObservationRegistry {
5 | associatedtype S: StorageAware
6 |
7 | /**
8 | Registers observation closure which will be removed automatically
9 | when the weakly captured observer has been deallocated.
10 | - Parameter observer: Any object that helps determine if the observation is still valid
11 | - Parameter closure: Observation closure
12 | - Returns: Token used to cancel the observation and remove the observation closure
13 | */
14 | @discardableResult
15 | func addStorageObserver(
16 | _ observer: O,
17 | closure: @escaping (O, S, StorageChange) -> Void
18 | ) -> ObservationToken
19 |
20 | /// Removes all registered key observers
21 | func removeAllStorageObservers()
22 | }
23 |
24 | // MARK: - StorageChange
25 |
26 | public enum StorageChange: Equatable {
27 | case add(key: String)
28 | case remove(key: String)
29 | case removeAll
30 | case removeExpired
31 | }
32 |
33 | public func == (lhs: StorageChange, rhs: StorageChange) -> Bool {
34 | switch (lhs, rhs) {
35 | case (.add(let key1), .add(let key2)), (.remove(let key1), .remove(let key2)):
36 | return key1 == key2
37 | case (.removeAll, .removeAll), (.removeExpired, .removeExpired):
38 | return true
39 | default:
40 | return false
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Library/ObjectConverterTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class JSONDecoderExtensionsTests: XCTestCase {
5 | private var storage: HybridStorage!
6 |
7 | override func setUp() {
8 | super.setUp()
9 | let memory = MemoryStorage(config: MemoryConfig())
10 | let disk = try! DiskStorage(
11 | config: DiskConfig(name: "HybridDisk"),
12 | transformer: TransformerFactory.forCodable(ofType: User.self)
13 | )
14 |
15 | storage = HybridStorage(memoryStorage: memory, diskStorage: disk)
16 | }
17 |
18 | override func tearDown() {
19 | try? storage.removeAll()
20 | super.tearDown()
21 | }
22 |
23 | func testJsonDictionary() throws {
24 | let json: [String: Any] = [
25 | "first_name": "John",
26 | "last_name": "Snow"
27 | ]
28 |
29 | let user = try JSONDecoder.decode(json, to: User.self)
30 | try storage.setObject(user, forKey: "user")
31 |
32 | let cachedObject = try storage.object(forKey: "user")
33 | XCTAssertEqual(user, cachedObject)
34 | }
35 |
36 | func testJsonString() throws {
37 | let string: String = "{\"first_name\": \"John\", \"last_name\": \"Snow\"}"
38 |
39 | let user = try JSONDecoder.decode(string, to: User.self)
40 | try storage.setObject(user, forKey: "user")
41 |
42 | let cachedObject = try storage.object(forKey: "user")
43 | XCTAssertEqual(cachedObject.firstName, "John")
44 | }
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Source/Shared/Configuration/DiskConfig.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct DiskConfig {
4 | /// The name of disk storage, this will be used as folder name within directory
5 | public let name: String
6 | /// Expiry date that will be applied by default for every added object
7 | /// if it's not overridden in the add(key: object: expiry: completion:) method
8 | public let expiry: Expiry
9 | /// Maximum size of the disk cache storage (in bytes)
10 | public let maxSize: UInt
11 | /// A folder to store the disk cache contents. Defaults to a prefixed directory in Caches if nil
12 | public let directory: URL?
13 | #if os(iOS) || os(tvOS)
14 | /// Data protection is used to store files in an encrypted format on disk and to decrypt them on demand.
15 | /// Support only on iOS and tvOS.
16 | public let protectionType: FileProtectionType?
17 |
18 | public init(name: String, expiry: Expiry = .never,
19 | maxSize: UInt = 0, directory: URL? = nil,
20 | protectionType: FileProtectionType? = nil) {
21 | self.name = name
22 | self.expiry = expiry
23 | self.maxSize = maxSize
24 | self.directory = directory
25 | self.protectionType = protectionType
26 | }
27 | #else
28 | public init(name: String, expiry: Expiry = .never,
29 | maxSize: UInt = 0, directory: URL? = nil) {
30 | self.name = name
31 | self.expiry = expiry
32 | self.maxSize = maxSize
33 | self.directory = directory
34 | }
35 | #endif
36 | }
37 |
--------------------------------------------------------------------------------
/Source/Shared/Extensions/JSONDecoder+Extensions.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Convert json string, dictionary, data to Codable objects
4 | public extension JSONDecoder {
5 | /// Convert json string to Codable object
6 | ///
7 | /// - Parameters:
8 | /// - string: Json string.
9 | /// - type: Type information.
10 | /// - Returns: Codable object.
11 | /// - Throws: Error if failed.
12 | static func decode(_ string: String, to type: T.Type) throws -> T {
13 | guard let data = string.data(using: .utf8) else {
14 | throw StorageError.decodingFailed
15 | }
16 |
17 | return try decode(data, to: type.self)
18 | }
19 |
20 | /// Convert json dictionary to Codable object
21 | ///
22 | /// - Parameters:
23 | /// - json: Json dictionary.
24 | /// - type: Type information.
25 | /// - Returns: Codable object
26 | /// - Throws: Error if failed
27 | static func decode(_ json: [String: Any], to type: T.Type) throws -> T {
28 | let data = try JSONSerialization.data(withJSONObject: json, options: [])
29 | return try decode(data, to: type)
30 | }
31 |
32 | /// Convert json data to Codable object
33 | ///
34 | /// - Parameters:
35 | /// - json: Json dictionary.
36 | /// - type: Type information.
37 | /// - Returns: Codable object
38 | /// - Throws: Error if failed
39 | static func decode(_ data: Data, to type: T.Type) throws -> T {
40 | return try JSONDecoder().decode(T.self, from: data)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Storage/SyncStorageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import Dispatch
3 | @testable import Cache
4 |
5 | final class SyncStorageTests: XCTestCase {
6 | private var storage: SyncStorage!
7 | let user = User(firstName: "John", lastName: "Snow")
8 |
9 | override func setUp() {
10 | super.setUp()
11 |
12 | let memory = MemoryStorage(config: MemoryConfig())
13 | let disk = try! DiskStorage(config: DiskConfig(name: "HybridDisk"), transformer: TransformerFactory.forCodable(ofType: User.self))
14 |
15 | let hybridStorage = HybridStorage(memoryStorage: memory, diskStorage: disk)
16 | storage = SyncStorage(storage: hybridStorage, serialQueue: DispatchQueue(label: "Sync"))
17 | }
18 |
19 | override func tearDown() {
20 | try? storage.removeAll()
21 | super.tearDown()
22 | }
23 |
24 | func testSetObject() throws {
25 | try storage.setObject(user, forKey: "user")
26 | let cachedObject = try storage.object(forKey: "user")
27 |
28 | XCTAssertEqual(cachedObject, user)
29 | }
30 |
31 | func testRemoveAll() throws {
32 | let intStorage = storage.transform(transformer: TransformerFactory.forCodable(ofType: Int.self))
33 | try given("add a lot of objects") {
34 | try Array(0..<100).forEach {
35 | try intStorage.setObject($0, forKey: "key-\($0)")
36 | }
37 | }
38 |
39 | try when("remove all") {
40 | try intStorage.removeAll()
41 | }
42 |
43 | try then("all are removed") {
44 | XCTAssertFalse(try intStorage.existsObject(forKey: "key-99"))
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Playgrounds/SimpleStorage.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | //: Playground - noun: a place where people can play
2 | import PlaygroundSupport
3 | import UIKit
4 | import Cache
5 |
6 | struct User: Codable {
7 | let id: Int
8 | let firstName: String
9 | let lastName: String
10 |
11 | var name: String {
12 | return "\(firstName) \(lastName)"
13 | }
14 | }
15 |
16 | let diskConfig = DiskConfig(name: "UserCache")
17 | let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)
18 |
19 | let storage = try! Storage(
20 | diskConfig: diskConfig,
21 | memoryConfig: memoryConfig,
22 | transformer: TransformerFactory.forCodable(ofType: User.self)
23 | )
24 |
25 | let user = User(id: 1, firstName: "John", lastName: "Snow")
26 | let key = "\(user.id)"
27 |
28 | // Add objects to the cache
29 | try storage.setObject(user, forKey: key)
30 |
31 | // Fetch object from the cache
32 | storage.async.object(forKey: key) { result in
33 | switch result {
34 | case .value(let user):
35 | print(user.name)
36 | case .error(let error):
37 | print(error)
38 | }
39 | }
40 |
41 | // Remove object from the cache
42 | try storage.removeObject(forKey: key)
43 |
44 | // Try to fetch removed object from the cache
45 | storage.async.object(forKey: key) { result in
46 | switch result {
47 | case .value(let user):
48 | print(user.name)
49 | case .error:
50 | print("no such object")
51 | }
52 | }
53 |
54 | // Clear cache
55 | try storage.removeAll()
56 |
57 | PlaygroundPage.current.needsIndefiniteExecution = true
58 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/KeyObservationRegistry.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A protocol used for adding and removing key observations
4 | public protocol KeyObservationRegistry {
5 | associatedtype S: StorageAware
6 |
7 | /**
8 | Registers observation closure which will be removed automatically
9 | when the weakly captured observer has been deallocated.
10 | - Parameter observer: Any object that helps determine if the observation is still valid
11 | - Parameter key: Unique key to identify the object in the cache
12 | - Parameter closure: Observation closure
13 | - Returns: Token used to cancel the observation and remove the observation closure
14 | */
15 | @discardableResult
16 | func addObserver(
17 | _ observer: O,
18 | forKey key: String,
19 | closure: @escaping (O, S, KeyChange) -> Void
20 | ) -> ObservationToken
21 |
22 | /**
23 | Removes observer by the given key.
24 | - Parameter key: Unique key to identify the object in the cache
25 | */
26 | func removeObserver(forKey key: String)
27 |
28 | /// Removes all registered key observers
29 | func removeAllKeyObservers()
30 | }
31 |
32 | // MARK: - KeyChange
33 |
34 | public enum KeyChange {
35 | case edit(before: T?, after: T)
36 | case remove
37 | }
38 |
39 | extension KeyChange: Equatable where T: Equatable {
40 | public static func == (lhs: KeyChange, rhs: KeyChange) -> Bool {
41 | switch (lhs, rhs) {
42 | case (.edit(let before1, let after1), .edit(let before2, let after2)):
43 | return before1 == before2 && after1 == after2
44 | case (.remove, .remove):
45 | return true
46 | default:
47 | return false
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/SyncStorage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Dispatch
3 |
4 | /// Manipulate storage in a "all sync" manner.
5 | /// Block the current queue until the operation completes.
6 | public class SyncStorage {
7 | public let innerStorage: HybridStorage
8 | public let serialQueue: DispatchQueue
9 |
10 | public init(storage: HybridStorage, serialQueue: DispatchQueue) {
11 | self.innerStorage = storage
12 | self.serialQueue = serialQueue
13 | }
14 | }
15 |
16 | extension SyncStorage: StorageAware {
17 | public func entry(forKey key: String) throws -> Entry {
18 | var entry: Entry!
19 | try serialQueue.sync {
20 | entry = try innerStorage.entry(forKey: key)
21 | }
22 |
23 | return entry
24 | }
25 |
26 | public func removeObject(forKey key: String) throws {
27 | try serialQueue.sync {
28 | try self.innerStorage.removeObject(forKey: key)
29 | }
30 | }
31 |
32 | public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws {
33 | try serialQueue.sync {
34 | try innerStorage.setObject(object, forKey: key, expiry: expiry)
35 | }
36 | }
37 |
38 | public func removeAll() throws {
39 | try serialQueue.sync {
40 | try innerStorage.removeAll()
41 | }
42 | }
43 |
44 | public func removeExpiredObjects() throws {
45 | try serialQueue.sync {
46 | try innerStorage.removeExpiredObjects()
47 | }
48 | }
49 | }
50 |
51 | public extension SyncStorage {
52 | func transform(transformer: Transformer) -> SyncStorage {
53 | let storage = SyncStorage(
54 | storage: innerStorage.transform(transformer: transformer),
55 | serialQueue: serialQueue
56 | )
57 |
58 | return storage
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/MemoryStorage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class MemoryStorage: StorageAware {
4 | fileprivate let cache = NSCache()
5 | // Memory cache keys
6 | fileprivate var keys = Set()
7 | /// Configuration
8 | fileprivate let config: MemoryConfig
9 |
10 | public init(config: MemoryConfig) {
11 | self.config = config
12 | self.cache.countLimit = Int(config.countLimit)
13 | self.cache.totalCostLimit = Int(config.totalCostLimit)
14 | }
15 | }
16 |
17 | extension MemoryStorage {
18 | public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) {
19 | let capsule = MemoryCapsule(value: object, expiry: .date(expiry?.date ?? config.expiry.date))
20 | cache.setObject(capsule, forKey: NSString(string: key))
21 | keys.insert(key)
22 | }
23 |
24 | public func removeAll() {
25 | cache.removeAllObjects()
26 | keys.removeAll()
27 | }
28 |
29 | public func removeExpiredObjects() {
30 | let allKeys = keys
31 | for key in allKeys {
32 | removeObjectIfExpired(forKey: key)
33 | }
34 | }
35 |
36 | public func removeObjectIfExpired(forKey key: String) {
37 | if let capsule = cache.object(forKey: NSString(string: key)), capsule.expiry.isExpired {
38 | removeObject(forKey: key)
39 | }
40 | }
41 |
42 | public func removeObject(forKey key: String) {
43 | cache.removeObject(forKey: NSString(string: key))
44 | keys.remove(key)
45 | }
46 |
47 | public func entry(forKey key: String) throws -> Entry {
48 | guard let capsule = cache.object(forKey: NSString(string: key)) else {
49 | throw StorageError.notFound
50 | }
51 |
52 | guard let object = capsule.object as? T else {
53 | throw StorageError.typeNotMatch
54 | }
55 |
56 | return Entry(object: object, expiry: capsule.expiry)
57 | }
58 | }
59 |
60 | public extension MemoryStorage {
61 | func transform() -> MemoryStorage {
62 | let storage = MemoryStorage(config: config)
63 | return storage
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Storage/AsyncStorageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import Dispatch
3 | @testable import Cache
4 |
5 | final class AsyncStorageTests: XCTestCase {
6 | private var storage: AsyncStorage!
7 | let user = User(firstName: "John", lastName: "Snow")
8 |
9 | override func setUp() {
10 | super.setUp()
11 | let memory = MemoryStorage(config: MemoryConfig())
12 | let disk = try! DiskStorage(config: DiskConfig(name: "Async Disk"), transformer: TransformerFactory.forCodable(ofType: User.self))
13 | let hybrid = HybridStorage(memoryStorage: memory, diskStorage: disk)
14 | storage = AsyncStorage(storage: hybrid, serialQueue: DispatchQueue(label: "Async"))
15 | }
16 |
17 | override func tearDown() {
18 | storage.removeAll(completion: { _ in })
19 | super.tearDown()
20 | }
21 |
22 | func testSetObject() throws {
23 | let expectation = self.expectation(description: #function)
24 |
25 | storage.setObject(user, forKey: "user", completion: { _ in })
26 | storage.object(forKey: "user", completion: { result in
27 | switch result {
28 | case .value(let cachedUser):
29 | XCTAssertEqual(cachedUser, self.user)
30 | expectation.fulfill()
31 | default:
32 | XCTFail()
33 | }
34 | })
35 |
36 | wait(for: [expectation], timeout: 1)
37 | }
38 |
39 | func testRemoveAll() {
40 | let intStorage = storage.transform(transformer: TransformerFactory.forCodable(ofType: Int.self))
41 | let expectation = self.expectation(description: #function)
42 | given("add a lot of objects") {
43 | Array(0..<100).forEach {
44 | intStorage.setObject($0, forKey: "key-\($0)", completion: { _ in })
45 | }
46 | }
47 |
48 | when("remove all") {
49 | intStorage.removeAll(completion: { _ in })
50 | }
51 |
52 | then("all are removed") {
53 | intStorage.existsObject(forKey: "key-99", completion: { result in
54 | switch result {
55 | case .value:
56 | XCTFail()
57 | default:
58 | expectation.fulfill()
59 | }
60 | })
61 | }
62 |
63 | wait(for: [expectation], timeout: 1)
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/StorageAware.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// A protocol used for saving and loading from storage
4 | public protocol StorageAware {
5 | associatedtype T
6 | /**
7 | Tries to retrieve the object from the storage.
8 | - Parameter key: Unique key to identify the object in the cache
9 | - Returns: Cached object or nil if not found
10 | */
11 | func object(forKey key: String) throws -> T
12 |
13 | /**
14 | Get cache entry which includes object with metadata.
15 | - Parameter key: Unique key to identify the object in the cache
16 | - Returns: Object wrapper with metadata or nil if not found
17 | */
18 | func entry(forKey key: String) throws -> Entry
19 |
20 | /**
21 | Removes the object by the given key.
22 | - Parameter key: Unique key to identify the object.
23 | */
24 | func removeObject(forKey key: String) throws
25 |
26 | /**
27 | Saves passed object.
28 | - Parameter key: Unique key to identify the object in the cache.
29 | - Parameter object: Object that needs to be cached.
30 | - Parameter expiry: Overwrite expiry for this object only.
31 | */
32 | func setObject(_ object: T, forKey key: String, expiry: Expiry?) throws
33 |
34 | /**
35 | Check if an object exist by the given key.
36 | - Parameter key: Unique key to identify the object.
37 | */
38 | func existsObject(forKey key: String) throws -> Bool
39 |
40 | /**
41 | Removes all objects from the cache storage.
42 | */
43 | func removeAll() throws
44 |
45 | /**
46 | Clears all expired objects.
47 | */
48 | func removeExpiredObjects() throws
49 |
50 | /**
51 | Check if an expired object by the given key.
52 | - Parameter key: Unique key to identify the object.
53 | */
54 | func isExpiredObject(forKey key: String) throws -> Bool
55 | }
56 |
57 | public extension StorageAware {
58 | func object(forKey key: String) throws -> T {
59 | return try entry(forKey: key).object
60 | }
61 |
62 | func existsObject(forKey key: String) throws -> Bool {
63 | do {
64 | let _: T = try object(forKey: key)
65 | return true
66 | } catch {
67 | return false
68 | }
69 | }
70 |
71 | func isExpiredObject(forKey key: String) throws -> Bool {
72 | do {
73 | let entry = try self.entry(forKey: key)
74 | return entry.expiry.isExpired
75 | } catch {
76 | return true
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Playgrounds/Storage.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | //: Playground - noun: a place where people can play
2 | import PlaygroundSupport
3 | import UIKit
4 | import Cache
5 |
6 | struct Helper {
7 | static func image(_ color: UIColor = .red, size: CGSize = .init(width: 1, height: 1)) -> UIImage {
8 | UIGraphicsBeginImageContextWithOptions(size, false, 1)
9 |
10 | let context = UIGraphicsGetCurrentContext()
11 | context?.setFillColor(color.cgColor)
12 | context?.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))
13 |
14 | let image = UIGraphicsGetImageFromCurrentImageContext()
15 | UIGraphicsEndImageContext()
16 |
17 | return image!
18 | }
19 |
20 | static func data(length: Int) -> Data {
21 | var buffer = [UInt8](repeating: 0, count: length)
22 | return Data(bytes: &buffer, count: length)
23 | }
24 | }
25 |
26 | // MARK: - Storage
27 |
28 | let diskConfig = DiskConfig(name: "Mix")
29 |
30 | let dataStorage = try! Storage(
31 | diskConfig: diskConfig,
32 | memoryConfig: MemoryConfig(),
33 | transformer: TransformerFactory.forData()
34 | )
35 |
36 | let stringStorage = dataStorage.transformCodable(ofType: String.self)
37 | let imageStorage = dataStorage.transformImage()
38 | let dateStorage = dataStorage.transformCodable(ofType: Date.self)
39 |
40 | // We already have Codable conformances for:
41 | // String, UIImage, NSData and NSDate (just for fun =)
42 |
43 | let string = "This is a string"
44 | let image = Helper.image()
45 | let data = Helper.data(length: 64)
46 | let date = Date(timeInterval: 100000, since: Date())
47 |
48 | // Add objects to the cache
49 | try stringStorage.setObject(string, forKey: "string")
50 | try imageStorage.setObject(image, forKey: "image")
51 | try dataStorage.setObject(data, forKey: "data")
52 | try dateStorage.setObject(date, forKey: "date")
53 | //
54 | //// Get objects from the cache
55 | let cachedString = try? stringStorage.object(forKey: "string")
56 | print(cachedString as Any)
57 |
58 | imageStorage.async.object(forKey: "image") { result in
59 | if case .value(let image) = result {
60 | print(image)
61 | }
62 | }
63 |
64 | dataStorage.async.object(forKey: "data") { result in
65 | if case .value(let data) = result {
66 | print(data)
67 | }
68 | }
69 |
70 | dateStorage.async.object(forKey: "date") { result in
71 | if case .value(let date) = result {
72 | print(date)
73 | }
74 | }
75 |
76 | // Clean the cache
77 | dataStorage.async.removeAll(completion: { (result) in
78 | if case .value = result {
79 | print("Cache cleaned")
80 | }
81 | })
82 |
83 | PlaygroundPage.current.needsIndefiniteExecution = true
84 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Library/TypeWrapperTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class TypeWrapperTests: XCTestCase {
5 | func testString() {
6 | let value = "Hello"
7 | let wrapper = TypeWrapper(object: value)
8 |
9 | let data = try! JSONEncoder().encode(wrapper)
10 | let anotherWrapper = try! JSONDecoder().decode(TypeWrapper.self, from: data)
11 |
12 | XCTAssertEqual(value, anotherWrapper.object)
13 | }
14 |
15 | func testInt() {
16 | let value = 10
17 | let wrapper = TypeWrapper(object: value)
18 |
19 | let data = try! JSONEncoder().encode(wrapper)
20 | let anotherWrapper = try! JSONDecoder().decode(TypeWrapper.self, from: data)
21 |
22 | XCTAssertEqual(value, anotherWrapper.object)
23 | }
24 |
25 | func testDate() {
26 | let value = Date()
27 | let wrapper = TypeWrapper(object: value)
28 |
29 | let data = try! JSONEncoder().encode(wrapper)
30 | let anotherWrapper = try! JSONDecoder().decode(TypeWrapper.self, from: data)
31 |
32 | XCTAssertEqual(value, anotherWrapper.object)
33 | }
34 |
35 | func testBool() {
36 | let value = true
37 | let wrapper = TypeWrapper(object: value)
38 |
39 | let data = try! JSONEncoder().encode(wrapper)
40 | let anotherWrapper = try! JSONDecoder().decode(TypeWrapper.self, from: data)
41 |
42 | XCTAssertEqual(value, anotherWrapper.object)
43 | }
44 |
45 | func testData() {
46 | let value = "Hello".data(using: .utf8)!
47 | let wrapper = TypeWrapper(object: value)
48 |
49 | let data = try! JSONEncoder().encode(wrapper)
50 | let anotherWrapper = try! JSONDecoder().decode(TypeWrapper.self, from: data)
51 |
52 | XCTAssertEqual(value, anotherWrapper.object)
53 | }
54 |
55 | func testArray() {
56 | let value = [1, 2, 3]
57 | let wrapper = TypeWrapper(object: value)
58 |
59 | let data = try! JSONEncoder().encode(wrapper)
60 | let anotherWrapper = try! JSONDecoder().decode(TypeWrapper>.self, from: data)
61 |
62 | XCTAssertEqual(value, anotherWrapper.object)
63 | }
64 |
65 | func testDictionary() {
66 | let value = [
67 | "key1": 1,
68 | "key2": 2
69 | ]
70 |
71 | let wrapper = TypeWrapper(object: value)
72 |
73 | let data = try! JSONEncoder().encode(wrapper)
74 | let anotherWrapper = try! JSONDecoder().decode(TypeWrapper>.self, from: data)
75 |
76 | XCTAssertEqual(value, anotherWrapper.object)
77 | }
78 |
79 | func testSet() {
80 | let value = Set(arrayLiteral: 1, 2, 3)
81 | let wrapper = TypeWrapper(object: value)
82 |
83 | let data = try! JSONEncoder().encode(wrapper)
84 | let anotherWrapper = try! JSONDecoder().decode(TypeWrapper>.self, from: data)
85 |
86 | XCTAssertEqual(value, anotherWrapper.object)
87 | }
88 | }
89 |
90 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/AsyncStorage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Dispatch
3 |
4 | /// Manipulate storage in a "all async" manner.
5 | /// The completion closure will be called when operation completes.
6 | public class AsyncStorage {
7 | public let innerStorage: HybridStorage
8 | public let serialQueue: DispatchQueue
9 |
10 | public init(storage: HybridStorage, serialQueue: DispatchQueue) {
11 | self.innerStorage = storage
12 | self.serialQueue = serialQueue
13 | }
14 | }
15 |
16 | extension AsyncStorage {
17 | public func entry(forKey key: String, completion: @escaping (Result>) -> Void) {
18 | serialQueue.async { [weak self] in
19 | guard let `self` = self else {
20 | completion(Result.error(StorageError.deallocated))
21 | return
22 | }
23 |
24 | do {
25 | let anEntry = try self.innerStorage.entry(forKey: key)
26 | completion(Result.value(anEntry))
27 | } catch {
28 | completion(Result.error(error))
29 | }
30 | }
31 | }
32 |
33 | public func removeObject(forKey key: String, completion: @escaping (Result<()>) -> Void) {
34 | serialQueue.async { [weak self] in
35 | guard let `self` = self else {
36 | completion(Result.error(StorageError.deallocated))
37 | return
38 | }
39 |
40 | do {
41 | try self.innerStorage.removeObject(forKey: key)
42 | completion(Result.value(()))
43 | } catch {
44 | completion(Result.error(error))
45 | }
46 | }
47 | }
48 |
49 | public func setObject(
50 | _ object: T,
51 | forKey key: String,
52 | expiry: Expiry? = nil,
53 | completion: @escaping (Result<()>) -> Void) {
54 | serialQueue.async { [weak self] in
55 | guard let `self` = self else {
56 | completion(Result.error(StorageError.deallocated))
57 | return
58 | }
59 |
60 | do {
61 | try self.innerStorage.setObject(object, forKey: key, expiry: expiry)
62 | completion(Result.value(()))
63 | } catch {
64 | completion(Result.error(error))
65 | }
66 | }
67 | }
68 |
69 | public func removeAll(completion: @escaping (Result<()>) -> Void) {
70 | serialQueue.async { [weak self] in
71 | guard let `self` = self else {
72 | completion(Result.error(StorageError.deallocated))
73 | return
74 | }
75 |
76 | do {
77 | try self.innerStorage.removeAll()
78 | completion(Result.value(()))
79 | } catch {
80 | completion(Result.error(error))
81 | }
82 | }
83 | }
84 |
85 | public func removeExpiredObjects(completion: @escaping (Result<()>) -> Void) {
86 | serialQueue.async { [weak self] in
87 | guard let `self` = self else {
88 | completion(Result.error(StorageError.deallocated))
89 | return
90 | }
91 |
92 | do {
93 | try self.innerStorage.removeExpiredObjects()
94 | completion(Result.value(()))
95 | } catch {
96 | completion(Result.error(error))
97 | }
98 | }
99 | }
100 |
101 | public func object(forKey key: String, completion: @escaping (Result) -> Void) {
102 | entry(forKey: key, completion: { (result: Result>) in
103 | completion(result.map({ entry in
104 | return entry.object
105 | }))
106 | })
107 | }
108 |
109 | public func existsObject(
110 | forKey key: String,
111 | completion: @escaping (Result) -> Void) {
112 | object(forKey: key, completion: { (result: Result) in
113 | completion(result.map({ _ in
114 | return true
115 | }))
116 | })
117 | }
118 | }
119 |
120 | public extension AsyncStorage {
121 | func transform(transformer: Transformer) -> AsyncStorage {
122 | let storage = AsyncStorage(
123 | storage: innerStorage.transform(transformer: transformer),
124 | serialQueue: serialQueue
125 | )
126 |
127 | return storage
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/Storage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Dispatch
3 |
4 | /// Manage storage. Use memory storage if specified.
5 | /// Synchronous by default. Use `async` for asynchronous operations.
6 | public final class Storage {
7 | /// Used for sync operations
8 | private let syncStorage: SyncStorage
9 | private let asyncStorage: AsyncStorage
10 | private let hybridStorage: HybridStorage
11 |
12 | /// Initialize storage with configuration options.
13 | ///
14 | /// - Parameters:
15 | /// - diskConfig: Configuration for disk storage
16 | /// - memoryConfig: Optional. Pass config if you want memory cache
17 | /// - Throws: Throw StorageError if any.
18 | public convenience init(diskConfig: DiskConfig, memoryConfig: MemoryConfig, transformer: Transformer) throws {
19 | let disk = try DiskStorage(config: diskConfig, transformer: transformer)
20 | let memory = MemoryStorage(config: memoryConfig)
21 | let hybridStorage = HybridStorage(memoryStorage: memory, diskStorage: disk)
22 | self.init(hybridStorage: hybridStorage)
23 | }
24 |
25 | /// Initialise with sync and async storages
26 | ///
27 | /// - Parameter syncStorage: Synchronous storage
28 | /// - Paraeter: asyncStorage: Asynchronous storage
29 | public init(hybridStorage: HybridStorage) {
30 | self.hybridStorage = hybridStorage
31 | self.syncStorage = SyncStorage(
32 | storage: hybridStorage,
33 | serialQueue: DispatchQueue(label: "Cache.SyncStorage.SerialQueue")
34 | )
35 | self.asyncStorage = AsyncStorage(
36 | storage: hybridStorage,
37 | serialQueue: DispatchQueue(label: "Cache.AsyncStorage.SerialQueue")
38 | )
39 | }
40 |
41 | /// Used for async operations
42 | public lazy var async = self.asyncStorage
43 | }
44 |
45 | extension Storage: StorageAware {
46 | public func entry(forKey key: String) throws -> Entry {
47 | return try self.syncStorage.entry(forKey: key)
48 | }
49 |
50 | public func removeObject(forKey key: String) throws {
51 | try self.syncStorage.removeObject(forKey: key)
52 | }
53 |
54 | public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws {
55 | try self.syncStorage.setObject(object, forKey: key, expiry: expiry)
56 | }
57 |
58 | public func removeAll() throws {
59 | try self.syncStorage.removeAll()
60 | }
61 |
62 | public func removeExpiredObjects() throws {
63 | try self.syncStorage.removeExpiredObjects()
64 | }
65 | }
66 |
67 | public extension Storage {
68 | func transform(transformer: Transformer) -> Storage {
69 | return Storage(hybridStorage: hybridStorage.transform(transformer: transformer))
70 | }
71 | }
72 |
73 | extension Storage: StorageObservationRegistry {
74 | @discardableResult
75 | public func addStorageObserver(
76 | _ observer: O,
77 | closure: @escaping (O, Storage, StorageChange) -> Void
78 | ) -> ObservationToken {
79 | return hybridStorage.addStorageObserver(observer) { [weak self] observer, _, change in
80 | guard let strongSelf = self else { return }
81 | closure(observer, strongSelf, change)
82 | }
83 | }
84 |
85 | public func removeAllStorageObservers() {
86 | hybridStorage.removeAllStorageObservers()
87 | }
88 | }
89 |
90 | extension Storage: KeyObservationRegistry {
91 | @discardableResult
92 | public func addObserver(
93 | _ observer: O,
94 | forKey key: String,
95 | closure: @escaping (O, Storage, KeyChange) -> Void
96 | ) -> ObservationToken {
97 | return hybridStorage.addObserver(observer, forKey: key) { [weak self] observer, _, change in
98 | guard let strongSelf = self else { return }
99 | closure(observer, strongSelf, change)
100 | }
101 | }
102 |
103 | public func removeObserver(forKey key: String) {
104 | hybridStorage.removeObserver(forKey: key)
105 | }
106 |
107 | public func removeAllKeyObservers() {
108 | hybridStorage.removeAllKeyObservers()
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/Cache.xcodeproj/xcshareddata/xcschemes/Cache-iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
35 |
41 |
42 |
43 |
44 |
45 |
51 |
52 |
53 |
54 |
55 |
56 |
66 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/Cache.xcodeproj/xcshareddata/xcschemes/Cache-tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
35 |
41 |
42 |
43 |
44 |
45 |
51 |
52 |
53 |
54 |
55 |
56 |
66 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Storage/MemoryStorageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class MemoryStorageTests: XCTestCase {
5 | private let key = "youknownothing"
6 | private let testObject = User(firstName: "John", lastName: "Snow")
7 | private var storage: MemoryStorage!
8 | private let config = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)
9 |
10 | override func setUp() {
11 | super.setUp()
12 | storage = MemoryStorage(config: config)
13 | }
14 |
15 | override func tearDown() {
16 | storage.removeAll()
17 | super.tearDown()
18 | }
19 |
20 | /// Test that it saves an object
21 | func testSetObject() {
22 | storage.setObject(testObject, forKey: key)
23 | let cachedObject = try! storage.object(forKey: key)
24 | XCTAssertNotNil(cachedObject)
25 | XCTAssertEqual(cachedObject.firstName, testObject.firstName)
26 | XCTAssertEqual(cachedObject.lastName, testObject.lastName)
27 | }
28 |
29 | func testCacheEntry() {
30 | // Returns nil if entry doesn't exist
31 | var entry = try? storage.entry(forKey: key)
32 | XCTAssertNil(entry)
33 |
34 | // Returns entry if object exists
35 | storage.setObject(testObject, forKey: key)
36 | entry = try! storage.entry(forKey: key)
37 |
38 | XCTAssertEqual(entry?.object.firstName, testObject.firstName)
39 | XCTAssertEqual(entry?.object.lastName, testObject.lastName)
40 | XCTAssertEqual(entry?.expiry.date, config.expiry.date)
41 | }
42 |
43 | func testSetObjectWithExpiry() {
44 | let date = Date().addingTimeInterval(1)
45 | storage.setObject(testObject, forKey: key, expiry: .seconds(1))
46 | var entry = try! storage.entry(forKey: key)
47 | XCTAssertEqual(entry.expiry.date.timeIntervalSinceReferenceDate,
48 | date.timeIntervalSinceReferenceDate,
49 | accuracy: 0.1)
50 | //Timer vs sleep: do not complicate
51 | sleep(1)
52 | entry = try! storage.entry(forKey: key)
53 | XCTAssertEqual(entry.expiry.date.timeIntervalSinceReferenceDate,
54 | date.timeIntervalSinceReferenceDate,
55 | accuracy: 0.1)
56 | }
57 |
58 | /// Test that it removes cached object
59 | func testRemoveObject() {
60 | storage.setObject(testObject, forKey: key)
61 | storage.removeObject(forKey: key)
62 | let cachedObject = try? storage.object(forKey: key)
63 | XCTAssertNil(cachedObject)
64 | }
65 |
66 | /// Test that it removes expired object
67 | func testRemoveObjectIfExpiredWhenExpired() {
68 | let expiry: Expiry = .date(Date().addingTimeInterval(-10))
69 | storage.setObject(testObject, forKey: key, expiry: expiry)
70 | storage.removeObjectIfExpired(forKey: key)
71 | let cachedObject = try? storage.object(forKey: key)
72 |
73 | XCTAssertNil(cachedObject)
74 | }
75 |
76 | /// Test that it doesn't remove not expired object
77 | func testRemoveObjectIfExpiredWhenNotExpired() {
78 | storage.setObject(testObject, forKey: key)
79 | storage.removeObjectIfExpired(forKey: key)
80 | let cachedObject = try! storage.object(forKey: key)
81 |
82 | XCTAssertNotNil(cachedObject)
83 | }
84 |
85 | /// Test expired object
86 | func testExpiredObject() throws {
87 | storage.setObject(testObject, forKey: key, expiry: .seconds(0.9))
88 | XCTAssertFalse(try! storage.isExpiredObject(forKey: key))
89 | sleep(1)
90 | XCTAssertTrue(try! storage.isExpiredObject(forKey: key))
91 | }
92 |
93 | /// Test that it clears cache directory
94 | func testRemoveAll() {
95 | storage.setObject(testObject, forKey: key)
96 | storage.removeAll()
97 | let cachedObject = try? storage.object(forKey: key)
98 | XCTAssertNil(cachedObject)
99 | }
100 |
101 | /// Test that it removes expired objects
102 | func testClearExpired() {
103 | let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
104 | let expiry2: Expiry = .date(Date().addingTimeInterval(10))
105 | let key1 = "item1"
106 | let key2 = "item2"
107 | storage.setObject(testObject, forKey: key1, expiry: expiry1)
108 | storage.setObject(testObject, forKey: key2, expiry: expiry2)
109 | storage.removeExpiredObjects()
110 | let object1 = try? storage.object(forKey: key1)
111 | let object2 = try! storage.object(forKey: key2)
112 |
113 | XCTAssertNil(object1)
114 | XCTAssertNotNil(object2)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Library/MD5Tests.swift:
--------------------------------------------------------------------------------
1 | // https://github.com/onmyway133/SwiftHash/blob/master/SwiftHashTests/Tests.swift
2 |
3 | import XCTest
4 | @testable import Cache
5 |
6 | class Tests: XCTestCase {
7 |
8 | func testHelper() {
9 | XCTAssertEqual(str2rstr_utf8("hello"), [104, 101, 108, 108, 111])
10 |
11 | let hello = str2rstr_utf8("hello")
12 | let world = str2rstr_utf8("world")
13 | let google = str2rstr_utf8("https://www.google.com")
14 |
15 | XCTAssertEqual(rstr2tr(hello), "hello")
16 | XCTAssertEqual(rstr2tr(world), "world")
17 | XCTAssertEqual(rstr2tr(google), "https://www.google.com")
18 |
19 | XCTAssertEqual(rstr2binl(hello), [1819043176, 111])
20 | XCTAssertEqual(rstr2binl(world), [1819438967, 100])
21 | XCTAssertEqual(rstr2binl(google), [1886680168, 791624307, 779581303, 1735356263, 1663985004, 28015])
22 |
23 | XCTAssertEqual(rstr2hex(hello), "68656C6C6F")
24 | XCTAssertEqual(rstr2hex(world), "776F726C64")
25 | XCTAssertEqual(rstr2hex(google), "68747470733A2F2F7777772E676F6F676C652E636F6D")
26 |
27 | XCTAssertEqual(Int32(-991732713) << Int32(12), 901869568)
28 | XCTAssertEqual(Int32(991732713) << Int32(12), -901869568)
29 | XCTAssertEqual(zeroFillRightShift(-991732713, (32 - 12)), 3150)
30 |
31 | XCTAssertEqual(bit_rol(1589092186, 17), 1052032367)
32 | XCTAssertEqual(bit_rol(-991732713, 12), 901872718)
33 |
34 | XCTAssertEqual(md5_cmn(-617265063, -615378706, -617265106, 0, 17, -1473231341), 434767261)
35 |
36 | XCTAssertEqual(md5_ff(271733878, -615343318, -271733879, -1732584194, 32879, 12, -389564586), 286529400)
37 |
38 | XCTAssertEqual(binl_md5(rstr2binl(hello), hello.count * 8), [708854109, 1982483388, -1851952711, -1832577264])
39 | XCTAssertEqual(binl_md5(rstr2binl(world), world.count * 8), [925923709, -2046724448, -2113778857, -415894286])
40 | }
41 |
42 | func testMD5() {
43 | XCTAssertEqual(MD5("hello"),
44 | "5D41402ABC4B2A76B9719D911017C592")
45 | XCTAssertEqual(MD5("world"),
46 | "7D793037A0760186574B0282F2F435E7")
47 | XCTAssertEqual(MD5("https://www.google.com"),
48 | "8FFDEFBDEC956B595D257F0AAEEFD623")
49 | XCTAssertEqual(MD5("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg"),
50 | "0DFB10E8D2AE771B3B3ED4544139644E")
51 | XCTAssertEqual(MD5("https://unsplash.it/600/300/?image=1"),
52 | "D59E956EBB1BE415970F04EC77F4C875")
53 | XCTAssertEqual(MD5(""),
54 | "D41D8CD98F00B204E9800998ECF8427E")
55 | XCTAssertEqual(MD5("ABCDEFGHIJKLMNOPQRSTWXYZ1234567890"),
56 | "B8F4F38629EC4F4A23F5DCC6086F8035")
57 | XCTAssertEqual(MD5("abcdefghijklmnopqrstwxyz1234567890"),
58 | "B2E875F4D53CCF6CEFB5CDA3F86FC542")
59 | XCTAssertEqual(MD5("0123456789"),
60 | "781E5E245D69B566979B86E28D23F2C7")
61 | XCTAssertEqual(MD5("0"),
62 | "CFCD208495D565EF66E7DFF9F98764DA")
63 | XCTAssertEqual(MD5("https://twitter.com/_HairForceOne/status/745235759460810752"),
64 | "40C2BFA3D7BFC7A453013ECD54022255")
65 | XCTAssertEqual(MD5("Det er et velkjent faktum at lesere distraheres av lesbart innhold på en side når man ser på dens layout. Poenget med å bruke Lorem Ipsum er at det har en mer eller mindre normal fordeling av bokstaver i ord, i motsetning til 'Innhold her, innhold her', og gir inntrykk av å være lesbar tekst. Mange webside- og sideombrekkingsprogrammer bruker nå Lorem Ipsum som sin standard for provisorisk tekst"),
66 | "6B2880BCC7554CF07E72DB9C99BF3284")
67 | XCTAssertEqual(MD5("\\"),
68 | "28D397E87306B8631F3ED80D858D35F0")
69 | XCTAssertEqual(MD5("http://res.cloudinary.com/demo/image/upload/w_300,h_200,c_crop/sample.jpg"),
70 | "6E30D9CC4C08BE4EEA49076328D4C1F0")
71 | XCTAssertEqual(MD5("http://res.cloudinary.com/demo/image/upload/x_355,y_410,w_300,h_200,c_crop/brown_sheep.jpg"),
72 | "019E9D72B5AF84EF114868875C1597ED")
73 | XCTAssertEqual(MD5("http://www.w3schools.com/tags/html_form_submit.asp?text=Hello+G%C3%BCnter"),
74 | "C89A2146CD3DF34ECDA86B6E0709B3FD")
75 | XCTAssertEqual(MD5("!%40%23%24%25%5E%26*()%2C.%3C%3E%5C'1234567890-%3D"),
76 | "09A1790760693160E74B9D6FCEC7EF64")
77 | }
78 |
79 | func testMD5_Data() {
80 | let data = "https://www.google.com".data(using: String.Encoding.utf8)
81 | XCTAssertEqual(MD5(String(data: data!, encoding: String.Encoding.utf8)!),
82 | "8FFDEFBDEC956B595D257F0AAEEFD623")
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Cache.xcodeproj/xcshareddata/xcschemes/Cache-macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
49 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
67 |
68 |
69 |
70 |
80 |
81 |
87 |
88 |
89 |
90 |
91 |
92 |
98 |
99 |
105 |
106 |
107 |
108 |
110 |
111 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/HybridStorage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Use both memory and disk storage. Try on memory first.
4 | public final class HybridStorage {
5 | public let memoryStorage: MemoryStorage
6 | public let diskStorage: DiskStorage
7 |
8 | private(set) var storageObservations = [UUID: (HybridStorage, StorageChange) -> Void]()
9 | private(set) var keyObservations = [String: (HybridStorage, KeyChange) -> Void]()
10 |
11 | public init(memoryStorage: MemoryStorage, diskStorage: DiskStorage) {
12 | self.memoryStorage = memoryStorage
13 | self.diskStorage = diskStorage
14 |
15 | diskStorage.onRemove = { [weak self] path in
16 | self?.handleRemovedObject(at: path)
17 | }
18 | }
19 |
20 | private func handleRemovedObject(at path: String) {
21 | notifyObserver(about: .remove) { key in
22 | let fileName = diskStorage.makeFileName(for: key)
23 | return path.contains(fileName)
24 | }
25 | }
26 | }
27 |
28 | extension HybridStorage: StorageAware {
29 | public func entry(forKey key: String) throws -> Entry {
30 | do {
31 | return try memoryStorage.entry(forKey: key)
32 | } catch {
33 | let entry = try diskStorage.entry(forKey: key)
34 | // set back to memoryStorage
35 | memoryStorage.setObject(entry.object, forKey: key, expiry: entry.expiry)
36 | return entry
37 | }
38 | }
39 |
40 | public func removeObject(forKey key: String) throws {
41 | memoryStorage.removeObject(forKey: key)
42 | try diskStorage.removeObject(forKey: key)
43 |
44 | notifyStorageObservers(about: .remove(key: key))
45 | }
46 |
47 | public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws {
48 | var keyChange: KeyChange?
49 |
50 | if keyObservations[key] != nil {
51 | keyChange = .edit(before: try? self.object(forKey: key), after: object)
52 | }
53 |
54 | memoryStorage.setObject(object, forKey: key, expiry: expiry)
55 | try diskStorage.setObject(object, forKey: key, expiry: expiry)
56 |
57 | if let change = keyChange {
58 | notifyObserver(forKey: key, about: change)
59 | }
60 |
61 | notifyStorageObservers(about: .add(key: key))
62 | }
63 |
64 | public func removeAll() throws {
65 | memoryStorage.removeAll()
66 | try diskStorage.removeAll()
67 |
68 | notifyStorageObservers(about: .removeAll)
69 | notifyKeyObservers(about: .remove)
70 | }
71 |
72 | public func removeExpiredObjects() throws {
73 | memoryStorage.removeExpiredObjects()
74 | try diskStorage.removeExpiredObjects()
75 |
76 | notifyStorageObservers(about: .removeExpired)
77 | }
78 | }
79 |
80 | public extension HybridStorage {
81 | func transform(transformer: Transformer) -> HybridStorage {
82 | let storage = HybridStorage(
83 | memoryStorage: memoryStorage.transform(),
84 | diskStorage: diskStorage.transform(transformer: transformer)
85 | )
86 |
87 | return storage
88 | }
89 | }
90 |
91 | extension HybridStorage: StorageObservationRegistry {
92 | @discardableResult
93 | public func addStorageObserver(
94 | _ observer: O,
95 | closure: @escaping (O, HybridStorage, StorageChange) -> Void
96 | ) -> ObservationToken {
97 | let id = UUID()
98 |
99 | storageObservations[id] = { [weak self, weak observer] storage, change in
100 | guard let observer = observer else {
101 | self?.storageObservations.removeValue(forKey: id)
102 | return
103 | }
104 |
105 | closure(observer, storage, change)
106 | }
107 |
108 | return ObservationToken { [weak self] in
109 | self?.storageObservations.removeValue(forKey: id)
110 | }
111 | }
112 |
113 | public func removeAllStorageObservers() {
114 | storageObservations.removeAll()
115 | }
116 |
117 | private func notifyStorageObservers(about change: StorageChange) {
118 | storageObservations.values.forEach { closure in
119 | closure(self, change)
120 | }
121 | }
122 | }
123 |
124 | extension HybridStorage: KeyObservationRegistry {
125 | @discardableResult
126 | public func addObserver(
127 | _ observer: O,
128 | forKey key: String,
129 | closure: @escaping (O, HybridStorage, KeyChange) -> Void
130 | ) -> ObservationToken {
131 | keyObservations[key] = { [weak self, weak observer] storage, change in
132 | guard let observer = observer else {
133 | self?.removeObserver(forKey: key)
134 | return
135 | }
136 |
137 | closure(observer, storage, change)
138 | }
139 |
140 | return ObservationToken { [weak self] in
141 | self?.keyObservations.removeValue(forKey: key)
142 | }
143 | }
144 |
145 | public func removeObserver(forKey key: String) {
146 | keyObservations.removeValue(forKey: key)
147 | }
148 |
149 | public func removeAllKeyObservers() {
150 | keyObservations.removeAll()
151 | }
152 |
153 | private func notifyObserver(forKey key: String, about change: KeyChange) {
154 | keyObservations[key]?(self, change)
155 | }
156 |
157 | private func notifyObserver(about change: KeyChange, whereKey closure: ((String) -> Bool)) {
158 | let observation = keyObservations.first { key, _ in closure(key) }?.value
159 | observation?(self, change)
160 | }
161 |
162 | private func notifyKeyObservers(about change: KeyChange) {
163 | keyObservations.values.forEach { closure in
164 | closure(self, change)
165 | }
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Storage/StorageSupportTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class StorageSupportTests: XCTestCase {
5 | private var storage: HybridStorage!
6 |
7 | override func setUp() {
8 | super.setUp()
9 | let memory = MemoryStorage(config: MemoryConfig())
10 | let disk = try! DiskStorage(config: DiskConfig(name: "PrimitiveDisk"), transformer: TransformerFactory.forCodable(ofType: Bool.self))
11 | storage = HybridStorage(memoryStorage: memory, diskStorage: disk)
12 | }
13 |
14 | override func tearDown() {
15 | try? storage.removeAll()
16 | super.tearDown()
17 | }
18 |
19 | func testSetPrimitive() throws {
20 | do {
21 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: Bool.self))
22 | try s.setObject(true, forKey: "bool")
23 | XCTAssertEqual(try s.object(forKey: "bool"), true)
24 | }
25 |
26 | do {
27 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: [Bool].self))
28 | try s.setObject([true, false, true], forKey: "array of bools")
29 | XCTAssertEqual(try s.object(forKey: "array of bools"), [true, false, true])
30 | }
31 |
32 | do {
33 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: String.self))
34 | try s.setObject("one", forKey: "string")
35 | XCTAssertEqual(try s.object(forKey: "string"), "one")
36 | }
37 |
38 | do {
39 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: [String].self))
40 | try s.setObject(["one", "two", "three"], forKey: "array of strings")
41 | XCTAssertEqual(try s.object(forKey: "array of strings"), ["one", "two", "three"])
42 | }
43 |
44 | do {
45 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: Int.self))
46 | try s.setObject(10, forKey: "int")
47 | XCTAssertEqual(try s.object(forKey: "int"), 10)
48 | }
49 |
50 | do {
51 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: [Int].self))
52 | try s.setObject([1, 2, 3], forKey: "array of ints")
53 | XCTAssertEqual(try s.object(forKey: "array of ints"), [1, 2, 3])
54 | }
55 |
56 | do {
57 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: Float.self))
58 | let float: Float = 1.1
59 | try s.setObject(float, forKey: "float")
60 | XCTAssertEqual(try s.object(forKey: "float"), float)
61 | }
62 |
63 | do {
64 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: [Float].self))
65 | let floats: [Float] = [1.1, 1.2, 1.3]
66 | try s.setObject(floats, forKey: "array of floats")
67 | XCTAssertEqual(try s.object(forKey: "array of floats"), floats)
68 | }
69 |
70 | do {
71 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: Double.self))
72 | let double: Double = 1.1
73 | try s.setObject(double, forKey: "double")
74 | XCTAssertEqual(try s.object(forKey: "double"), double)
75 | }
76 |
77 | do {
78 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: [Double].self))
79 | let doubles: [Double] = [1.1, 1.2, 1.3]
80 | try s.setObject(doubles, forKey: "array of doubles")
81 | XCTAssertEqual(try s.object(forKey: "array of doubles"), doubles)
82 | }
83 | }
84 |
85 | func testSetData() {
86 | let s = storage.transform(transformer: TransformerFactory.forData())
87 |
88 | do {
89 | let string = "Hello"
90 | let data = string.data(using: .utf8)!
91 | try s.setObject(data, forKey: "data")
92 |
93 | let cachedObject = try s.object(forKey: "data")
94 | let cachedString = String(data: cachedObject, encoding: .utf8)
95 |
96 | XCTAssertEqual(cachedString, string)
97 | } catch {
98 | XCTFail(error.localizedDescription)
99 | }
100 | }
101 |
102 | func testSetDate() throws {
103 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: Date.self))
104 |
105 | let date = Date(timeIntervalSince1970: 100)
106 | try s.setObject(date, forKey: "date")
107 | let cachedObject = try s.object(forKey: "date")
108 |
109 | XCTAssertEqual(date, cachedObject)
110 | }
111 |
112 | func testSetURL() throws {
113 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: URL.self))
114 | let url = URL(string: "https://hyper.no")!
115 | try s.setObject(url, forKey: "url")
116 | let cachedObject = try s.object(forKey: "url")
117 |
118 | XCTAssertEqual(url, cachedObject)
119 | }
120 |
121 | func testWithSet() throws {
122 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: Set.self))
123 | let set = Set(arrayLiteral: 1, 2, 3)
124 | try s.setObject(set, forKey: "set")
125 | XCTAssertEqual(try s.object(forKey: "set") as Set, set)
126 | }
127 |
128 | func testWithSimpleDictionary() throws {
129 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: [String: Int].self))
130 |
131 | let dict: [String: Int] = [
132 | "key1": 1,
133 | "key2": 2
134 | ]
135 |
136 | try s.setObject(dict, forKey: "dict")
137 | let cachedObject = try s.object(forKey: "dict") as [String: Int]
138 | XCTAssertEqual(cachedObject, dict)
139 | }
140 |
141 | func testWithComplexDictionary() {
142 | let _: [String: Any] = [
143 | "key1": 1,
144 | "key2": 2
145 | ]
146 |
147 | // fatal error: Dictionary does not conform to Encodable because Any does not conform to Encodable
148 | // try storage.setObject(dict, forKey: "dict")
149 | }
150 |
151 | func testIntFloat() throws {
152 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: Float.self))
153 | let key = "key"
154 | try s.setObject(10, forKey: key)
155 |
156 | try then("Casting to int or float is the same") {
157 | XCTAssertEqual(try s.object(forKey: key), 10)
158 |
159 | let intStorage = s.transform(transformer: TransformerFactory.forCodable(ofType: Int.self))
160 | XCTAssertEqual(try intStorage.object(forKey: key), 10)
161 | }
162 | }
163 |
164 | func testFloatDouble() throws {
165 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: Float.self))
166 | let key = "key"
167 | try s.setObject(10.5, forKey: key)
168 |
169 | try then("Casting to float or double is the same") {
170 | XCTAssertEqual(try s.object(forKey: key), 10.5)
171 |
172 | let doubleStorage = s.transform(transformer: TransformerFactory.forCodable(ofType: Double.self))
173 | XCTAssertEqual(try doubleStorage.object(forKey: key), 10.5)
174 | }
175 | }
176 |
177 | func testCastingToAnotherType() throws {
178 | let s = storage.transform(transformer: TransformerFactory.forCodable(ofType: String.self))
179 | try s.setObject("Hello", forKey: "string")
180 |
181 | do {
182 | let intStorage = s.transform(transformer: TransformerFactory.forCodable(ofType: Int.self))
183 | let _ = try intStorage.object(forKey: "string")
184 | XCTFail()
185 | } catch {
186 | XCTAssertTrue(error is DecodingError)
187 | }
188 | }
189 |
190 | func testOverridenOnDisk() throws {
191 | let intStorage = storage.transform(transformer: TransformerFactory.forCodable(ofType: Int.self))
192 | let stringStorage = storage.transform(transformer: TransformerFactory.forCodable(ofType: String.self))
193 |
194 | let key = "sameKey"
195 |
196 | try intStorage.setObject(1, forKey: key)
197 | try stringStorage.setObject("hello world", forKey: key)
198 |
199 | let intValue = try? intStorage.diskStorage.object(forKey: key)
200 | let stringValue = try? stringStorage.diskStorage.object(forKey: key)
201 |
202 | XCTAssertNil(intValue)
203 | XCTAssertNotNil(stringValue)
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Storage/DiskStorageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class DiskStorageTests: XCTestCase {
5 | private let key = "youknownothing"
6 | private let testObject = User(firstName: "John", lastName: "Snow")
7 | private let fileManager = FileManager()
8 | private var storage: DiskStorage!
9 | private let config = DiskConfig(name: "Floppy")
10 |
11 | override func setUp() {
12 | super.setUp()
13 | storage = try! DiskStorage(config: config, transformer: TransformerFactory.forCodable(ofType: User.self))
14 | }
15 |
16 | override func tearDown() {
17 | try? storage.removeAll()
18 | super.tearDown()
19 | }
20 |
21 | func testInit() {
22 | // Test that it creates cache directory
23 | let fileExist = fileManager.fileExists(atPath: storage.path)
24 | XCTAssertTrue(fileExist)
25 |
26 | // Test that it returns the default maximum size of a cache
27 | XCTAssertEqual(config.maxSize, 0)
28 | }
29 |
30 | /// Test that it returns the correct path
31 | func testDefaultPath() {
32 | let paths = NSSearchPathForDirectoriesInDomains(
33 | .cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true
34 | )
35 | let path = "\(paths.first!)/\(config.name.capitalized)"
36 | XCTAssertEqual(storage.path, path)
37 | }
38 |
39 | /// Test that it returns the correct path
40 | func testCustomPath() throws {
41 | let url = try fileManager.url(
42 | for: .applicationSupportDirectory,
43 | in: .userDomainMask,
44 | appropriateFor: nil,
45 | create: true
46 | )
47 |
48 | let customConfig = DiskConfig(name: "SSD", directory: url)
49 |
50 | storage = try DiskStorage(config: customConfig, transformer: TransformerFactory.forCodable(ofType: User.self))
51 |
52 | XCTAssertEqual(
53 | storage.path,
54 | url.appendingPathComponent("SSD", isDirectory: true).path
55 | )
56 | }
57 |
58 | /// Test that it sets attributes
59 | func testSetDirectoryAttributes() throws {
60 | try storage.setObject(testObject, forKey: key)
61 | try storage.setDirectoryAttributes([FileAttributeKey.immutable: true])
62 | let attributes = try fileManager.attributesOfItem(atPath: storage.path)
63 |
64 | XCTAssertTrue(attributes[FileAttributeKey.immutable] as? Bool == true)
65 | try storage.setDirectoryAttributes([FileAttributeKey.immutable: false])
66 | }
67 |
68 | /// Test that it saves an object
69 | func testsetObject() throws {
70 | try storage.setObject(testObject, forKey: key)
71 | let fileExist = fileManager.fileExists(atPath: storage.makeFilePath(for: key))
72 | XCTAssertTrue(fileExist)
73 | }
74 |
75 | /// Test that
76 | func testCacheEntry() throws {
77 | // Returns nil if entry doesn't exist
78 | var entry: Entry?
79 | do {
80 | entry = try storage.entry(forKey: key)
81 | } catch {}
82 | XCTAssertNil(entry)
83 |
84 | // Returns entry if object exists
85 | try storage.setObject(testObject, forKey: key)
86 | entry = try storage.entry(forKey: key)
87 | let attributes = try fileManager.attributesOfItem(atPath: storage.makeFilePath(for: key))
88 | let expiry = Expiry.date(attributes[FileAttributeKey.modificationDate] as! Date)
89 |
90 | XCTAssertEqual(entry?.object.firstName, testObject.firstName)
91 | XCTAssertEqual(entry?.object.lastName, testObject.lastName)
92 | XCTAssertEqual(entry?.expiry.date, expiry.date)
93 | }
94 |
95 | func testCacheEntryPath() throws {
96 | let key = "test.mp4"
97 | try storage.setObject(testObject, forKey: key)
98 | let entry = try storage.entry(forKey: key)
99 | let filePath = storage.makeFilePath(for: key)
100 |
101 | XCTAssertEqual(entry.filePath, filePath)
102 | }
103 |
104 | /// Test that it resolves cached object
105 | func testSetObject() throws {
106 | try storage.setObject(testObject, forKey: key)
107 | let cachedObject: User? = try storage.object(forKey: key)
108 |
109 | XCTAssertEqual(cachedObject?.firstName, testObject.firstName)
110 | XCTAssertEqual(cachedObject?.lastName, testObject.lastName)
111 | }
112 |
113 | /// Test that it removes cached object
114 | func testRemoveObject() throws {
115 | try storage.setObject(testObject, forKey: key)
116 | try storage.removeObject(forKey: key)
117 | let fileExist = fileManager.fileExists(atPath: storage.makeFilePath(for: key))
118 | XCTAssertFalse(fileExist)
119 | }
120 |
121 | /// Test that it removes expired object
122 | func testRemoveObjectIfExpiredWhenExpired() throws {
123 | let expiry: Expiry = .date(Date().addingTimeInterval(-100000))
124 | try storage.setObject(testObject, forKey: key, expiry: expiry)
125 | try storage.removeObjectIfExpired(forKey: key)
126 | var cachedObject: User?
127 | do {
128 | cachedObject = try storage.object(forKey: key)
129 | } catch {}
130 |
131 | XCTAssertNil(cachedObject)
132 | }
133 |
134 | /// Test that it doesn't remove not expired object
135 | func testRemoveObjectIfExpiredWhenNotExpired() throws {
136 | try storage.setObject(testObject, forKey: key)
137 | try storage.removeObjectIfExpired(forKey: key)
138 | let cachedObject: User? = try storage.object(forKey: key)
139 | XCTAssertNotNil(cachedObject)
140 | }
141 |
142 | /// Test expired object
143 | func testExpiredObject() throws {
144 | try storage.setObject(testObject, forKey: key, expiry: .seconds(0.9))
145 | XCTAssertFalse(try! storage.isExpiredObject(forKey: key))
146 | sleep(1)
147 | XCTAssertTrue(try! storage.isExpiredObject(forKey: key))
148 | }
149 |
150 | /// Test that it clears cache directory
151 | func testClear() throws {
152 | try given("create some files inside folder so that it is not empty") {
153 | try storage.setObject(testObject, forKey: key)
154 | }
155 |
156 | when("call removeAll to remove the whole the folder") {
157 | do {
158 | try storage.removeAll()
159 | } catch {
160 | XCTFail(error.localizedDescription)
161 | }
162 | }
163 |
164 | then("the folder should exist") {
165 | let fileExist = fileManager.fileExists(atPath: storage.path)
166 | XCTAssertTrue(fileExist)
167 | }
168 |
169 | then("the folder should be empty") {
170 | let contents = try? fileManager.contentsOfDirectory(atPath: storage.path)
171 | XCTAssertEqual(contents?.count, 0)
172 | }
173 | }
174 |
175 | /// Test that it clears cache files, but keeps root directory
176 | func testCreateDirectory() {
177 | do {
178 | try storage.removeAll()
179 | XCTAssertTrue(fileManager.fileExists(atPath: storage.path))
180 | let contents = try? fileManager.contentsOfDirectory(atPath: storage.path)
181 | XCTAssertEqual(contents?.count, 0)
182 | } catch {
183 | XCTFail(error.localizedDescription)
184 | }
185 | }
186 |
187 | /// Test that it removes expired objects
188 | func testClearExpired() throws {
189 | let expiry1: Expiry = .date(Date().addingTimeInterval(-100000))
190 | let expiry2: Expiry = .date(Date().addingTimeInterval(100000))
191 | let key1 = "item1"
192 | let key2 = "item2"
193 | try storage.setObject(testObject, forKey: key1, expiry: expiry1)
194 | try storage.setObject(testObject, forKey: key2, expiry: expiry2)
195 | try storage.removeExpiredObjects()
196 | var object1: User?
197 | let object2 = try storage.object(forKey: key2)
198 |
199 | do {
200 | object1 = try storage.object(forKey: key1)
201 | } catch {}
202 |
203 | XCTAssertNil(object1)
204 | XCTAssertNotNil(object2)
205 | }
206 |
207 | /// Test that it returns a correct file name
208 | func testMakeFileName() {
209 | XCTAssertEqual(storage.makeFileName(for: key), MD5(key))
210 | XCTAssertEqual(storage.makeFileName(for: "test.mp4"), "\(MD5("test.mp4")).mp4")
211 | }
212 |
213 | /// Test that it returns a correct file path
214 | func testMakeFilePath() {
215 | let filePath = "\(storage.path)/\(storage.makeFileName(for: key))"
216 | XCTAssertEqual(storage.makeFilePath(for: key), filePath)
217 | }
218 | }
219 |
220 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Storage/StorageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class StorageTests: XCTestCase {
5 | private var storage: Storage!
6 | let user = User(firstName: "John", lastName: "Snow")
7 |
8 | override func setUp() {
9 | super.setUp()
10 |
11 | storage = try! Storage(
12 | diskConfig: DiskConfig(name: "Thor"),
13 | memoryConfig: MemoryConfig(),
14 | transformer: TransformerFactory.forCodable(ofType: User.self)
15 | )
16 | }
17 |
18 | override func tearDown() {
19 | try? storage.removeAll()
20 | super.tearDown()
21 | }
22 |
23 | func testSync() throws {
24 | try storage.setObject(user, forKey: "user")
25 | let cachedObject = try storage.object(forKey: "user")
26 |
27 | XCTAssertEqual(cachedObject, user)
28 | }
29 |
30 | func testAsync() {
31 | let expectation = self.expectation(description: #function)
32 | storage.async.setObject(user, forKey: "user", expiry: nil, completion: { _ in })
33 |
34 | storage.async.object(forKey: "user", completion: { result in
35 | switch result {
36 | case .value(let cachedUser):
37 | XCTAssertEqual(cachedUser, self.user)
38 | expectation.fulfill()
39 | default:
40 | XCTFail()
41 | }
42 | })
43 |
44 | wait(for: [expectation], timeout: 1)
45 | }
46 |
47 | func testMigration() {
48 | struct Person1: Codable {
49 | let fullName: String
50 | }
51 |
52 | struct Person2: Codable {
53 | let firstName: String
54 | let lastName: String
55 | }
56 |
57 | let person1Storage = storage.transformCodable(ofType: Person1.self)
58 | let person2Storage = storage.transformCodable(ofType: Person2.self)
59 |
60 | // Firstly, save object of type Person1
61 | let person = Person1(fullName: "John Snow")
62 |
63 | try! person1Storage.setObject(person, forKey: "person")
64 | XCTAssertNil(try? person2Storage.object(forKey: "person"))
65 |
66 | // Later, convert to Person2, do the migration, then overwrite
67 | let tempPerson = try! person1Storage.object(forKey: "person")
68 | let parts = tempPerson.fullName.split(separator: " ")
69 | let migratedPerson = Person2(firstName: String(parts[0]), lastName: String(parts[1]))
70 | try! person2Storage.setObject(migratedPerson, forKey: "person")
71 |
72 | XCTAssertEqual(
73 | try! person2Storage.object(forKey: "person").firstName,
74 | "John"
75 | )
76 | }
77 |
78 | func testSameProperties() {
79 | struct Person: Codable {
80 | let firstName: String
81 | let lastName: String
82 | }
83 |
84 | struct Alien: Codable {
85 | let firstName: String
86 | let lastName: String
87 | }
88 |
89 | let personStorage = storage.transformCodable(ofType: Person.self)
90 | let alienStorage = storage.transformCodable(ofType: Alien.self)
91 |
92 | let person = Person(firstName: "John", lastName: "Snow")
93 | try! personStorage.setObject(person, forKey: "person")
94 |
95 | // As long as it has same properties, it works too
96 | let cachedObject = try! alienStorage.object(forKey: "person")
97 | XCTAssertEqual(cachedObject.firstName, "John")
98 | }
99 |
100 | // MARK: - Storage observers
101 |
102 | func testAddStorageObserver() throws {
103 | var changes = [StorageChange]()
104 | var observer: ObserverMock? = ObserverMock()
105 |
106 | storage.addStorageObserver(observer!) { _, _, change in
107 | changes.append(change)
108 | }
109 |
110 | try storage.setObject(user, forKey: "user1")
111 | try storage.setObject(user, forKey: "user2")
112 | try storage.removeObject(forKey: "user1")
113 | try storage.removeExpiredObjects()
114 | try storage.removeAll()
115 | observer = nil
116 | try storage.setObject(user, forKey: "user1")
117 |
118 | let expectedChanges: [StorageChange] = [
119 | .add(key: "user1"),
120 | .add(key: "user2"),
121 | .remove(key: "user1"),
122 | .removeExpired,
123 | .removeAll
124 | ]
125 |
126 | XCTAssertEqual(changes, expectedChanges)
127 | }
128 |
129 | func testRemoveAllStorageObservers() throws {
130 | var changes1 = [StorageChange]()
131 | var changes2 = [StorageChange]()
132 |
133 | storage.addStorageObserver(self) { _, _, change in
134 | changes1.append(change)
135 | }
136 |
137 | storage.addStorageObserver(self) { _, _, change in
138 | changes2.append(change)
139 | }
140 |
141 | try storage.setObject(user, forKey: "user1")
142 | XCTAssertEqual(changes1, [StorageChange.add(key: "user1")])
143 | XCTAssertEqual(changes2, [StorageChange.add(key: "user1")])
144 |
145 | changes1.removeAll()
146 | changes2.removeAll()
147 | storage.removeAllStorageObservers()
148 |
149 | try storage.setObject(user, forKey: "user1")
150 | XCTAssertTrue(changes1.isEmpty)
151 | XCTAssertTrue(changes2.isEmpty)
152 | }
153 |
154 | // MARK: - Key observers
155 |
156 | func testAddObserverForKey() throws {
157 | var changes = [KeyChange]()
158 | storage.addObserver(self, forKey: "user1") { _, _, change in
159 | changes.append(change)
160 | }
161 |
162 | storage.addObserver(self, forKey: "user2") { _, _, change in
163 | changes.append(change)
164 | }
165 |
166 | try storage.setObject(user, forKey: "user1")
167 | XCTAssertEqual(changes, [KeyChange.edit(before: nil, after: user)])
168 | }
169 |
170 | func testKeyObserverWithRemoveExpired() throws {
171 | var changes = [KeyChange]()
172 | storage.addObserver(self, forKey: "user1") { _, _, change in
173 | changes.append(change)
174 | }
175 |
176 | storage.addObserver(self, forKey: "user2") { _, _, change in
177 | changes.append(change)
178 | }
179 |
180 | try storage.setObject(user, forKey: "user1", expiry: Expiry.seconds(-1000))
181 | try storage.removeExpiredObjects()
182 |
183 | XCTAssertEqual(changes, [.edit(before: nil, after: user), .remove])
184 | }
185 |
186 | func testKeyObserverWithRemoveAll() throws {
187 | var changes1 = [KeyChange]()
188 | var changes2 = [KeyChange]()
189 |
190 | storage.addObserver(self, forKey: "user1") { _, _, change in
191 | changes1.append(change)
192 | }
193 |
194 | storage.addObserver(self, forKey: "user2") { _, _, change in
195 | changes2.append(change)
196 | }
197 |
198 | try storage.setObject(user, forKey: "user1")
199 | try storage.setObject(user, forKey: "user2")
200 | try storage.removeAll()
201 |
202 | XCTAssertEqual(changes1, [.edit(before: nil, after: user), .remove])
203 | XCTAssertEqual(changes2, [.edit(before: nil, after: user), .remove])
204 | }
205 |
206 | func testRemoveKeyObserver() throws {
207 | var changes = [KeyChange]()
208 |
209 | // Test remove
210 | storage.addObserver(self, forKey: "user1") { _, _, change in
211 | changes.append(change)
212 | }
213 |
214 | storage.removeObserver(forKey: "user1")
215 | try storage.setObject(user, forKey: "user1")
216 | XCTAssertTrue(changes.isEmpty)
217 |
218 | // Test remove by token
219 | let token = storage.addObserver(self, forKey: "user2") { _, _, change in
220 | changes.append(change)
221 | }
222 |
223 | token.cancel()
224 | try storage.setObject(user, forKey: "user1")
225 | XCTAssertTrue(changes.isEmpty)
226 | }
227 |
228 | func testRemoveAllKeyObservers() throws {
229 | var changes1 = [KeyChange]()
230 | var changes2 = [KeyChange]()
231 |
232 | storage.addObserver(self, forKey: "user1") { _, _, change in
233 | changes1.append(change)
234 | }
235 |
236 | storage.addObserver(self, forKey: "user2") { _, _, change in
237 | changes2.append(change)
238 | }
239 |
240 | try storage.setObject(user, forKey: "user1")
241 | try storage.setObject(user, forKey: "user2")
242 | XCTAssertEqual(changes1, [KeyChange.edit(before: nil, after: user)])
243 | XCTAssertEqual(changes2, [KeyChange.edit(before: nil, after: user)])
244 |
245 | changes1.removeAll()
246 | changes2.removeAll()
247 | storage.removeAllKeyObservers()
248 |
249 | try storage.setObject(user, forKey: "user1")
250 | XCTAssertTrue(changes1.isEmpty)
251 | XCTAssertTrue(changes2.isEmpty)
252 | }
253 | }
254 |
255 | private class ObserverMock {}
256 |
--------------------------------------------------------------------------------
/Tests/iOS/Tests/Storage/HybridStorageTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Cache
3 |
4 | final class HybridStorageTests: XCTestCase {
5 | private let cacheName = "WeirdoCache"
6 | private let key = "alongweirdkey"
7 | private let testObject = User(firstName: "John", lastName: "Targaryen")
8 | private var storage: HybridStorage!
9 | private let fileManager = FileManager()
10 |
11 | override func setUp() {
12 | super.setUp()
13 | let memory = MemoryStorage(config: MemoryConfig())
14 | let disk = try! DiskStorage(config: DiskConfig(name: "HybridDisk"), transformer: TransformerFactory.forCodable(ofType: User.self))
15 |
16 | storage = HybridStorage(memoryStorage: memory, diskStorage: disk)
17 | }
18 |
19 | override func tearDown() {
20 | try? storage.removeAll()
21 | super.tearDown()
22 | }
23 |
24 | func testSetObject() throws {
25 | try when("set to storage") {
26 | try storage.setObject(testObject, forKey: key)
27 | let cachedObject = try storage.object(forKey: key)
28 | XCTAssertEqual(cachedObject, testObject)
29 | }
30 |
31 | try then("it is set to memory too") {
32 | let memoryObject = try storage.memoryStorage.object(forKey: key)
33 | XCTAssertNotNil(memoryObject)
34 | }
35 |
36 | try then("it is set to disk too") {
37 | let diskObject = try storage.diskStorage.object(forKey: key)
38 | XCTAssertNotNil(diskObject)
39 | }
40 | }
41 |
42 | func testEntry() throws {
43 | let expiryDate = Date()
44 | try storage.setObject(testObject, forKey: key, expiry: .date(expiryDate))
45 | let entry = try storage.entry(forKey: key)
46 |
47 | XCTAssertEqual(entry.object, testObject)
48 | XCTAssertEqual(entry.expiry.date, expiryDate)
49 | }
50 |
51 | /// Should resolve from disk and set in-memory cache if object not in-memory
52 | func testObjectCopyToMemory() throws {
53 | try when("set to disk only") {
54 | try storage.diskStorage.setObject(testObject, forKey: key)
55 | let cachedObject: User = try storage.object(forKey: key)
56 | XCTAssertEqual(cachedObject, testObject)
57 | }
58 |
59 | try then("there is no object in memory") {
60 | let inMemoryCachedObject = try storage.memoryStorage.object(forKey: key)
61 | XCTAssertEqual(inMemoryCachedObject, testObject)
62 | }
63 | }
64 |
65 | func testEntityExpiryForObjectCopyToMemory() throws {
66 | let date = Date().addingTimeInterval(3)
67 | try when("set to disk only") {
68 | try storage.diskStorage.setObject(testObject, forKey: key, expiry: .seconds(3))
69 | let entry = try storage.entry(forKey: key)
70 | //accuracy for slow disk processes
71 | XCTAssertEqual(entry.expiry.date.timeIntervalSinceReferenceDate,
72 | date.timeIntervalSinceReferenceDate,
73 | accuracy: 1.0)
74 | }
75 |
76 | try then("there is no object in memory") {
77 | let entry = try storage.memoryStorage.entry(forKey: key)
78 | //accuracy for slow disk processes
79 | XCTAssertEqual(entry.expiry.date.timeIntervalSinceReferenceDate,
80 | date.timeIntervalSinceReferenceDate,
81 | accuracy: 1.0)
82 | }
83 | }
84 |
85 | /// Removes cached object from memory and disk
86 | func testRemoveObject() throws {
87 | try given("set to storage") {
88 | try storage.setObject(testObject, forKey: key)
89 | XCTAssertNotNil(try storage.object(forKey: key))
90 | }
91 |
92 | try when("remove object from storage") {
93 | try storage.removeObject(forKey: key)
94 | let cachedObject = try? storage.object(forKey: key)
95 | XCTAssertNil(cachedObject)
96 | }
97 |
98 | then("there is no object in memory") {
99 | let memoryObject = try? storage.memoryStorage.object(forKey: key)
100 | XCTAssertNil(memoryObject)
101 | }
102 |
103 | then("there is no object on disk") {
104 | let diskObject = try? storage.diskStorage.object(forKey: key)
105 | XCTAssertNil(diskObject)
106 | }
107 | }
108 |
109 | /// Clears memory and disk cache
110 | func testClear() throws {
111 | try when("set and remove all") {
112 | try storage.setObject(testObject, forKey: key)
113 | try storage.removeAll()
114 | XCTAssertNil(try? storage.object(forKey: key))
115 | }
116 |
117 | then("there is no object in memory") {
118 | let memoryObject = try? storage.memoryStorage.object(forKey: key)
119 | XCTAssertNil(memoryObject)
120 | }
121 |
122 | then("there is no object on disk") {
123 | let diskObject = try? storage.diskStorage.object(forKey: key)
124 | XCTAssertNil(diskObject)
125 | }
126 | }
127 |
128 | func testDiskEmptyAfterClear() throws {
129 | try storage.setObject(testObject, forKey: key)
130 | try storage.removeAll()
131 |
132 | then("the disk directory is empty") {
133 | let contents = try? fileManager.contentsOfDirectory(atPath: storage.diskStorage.path)
134 | XCTAssertEqual(contents?.count, 0)
135 | }
136 | }
137 |
138 | /// Clears expired objects from memory and disk cache
139 | func testClearExpired() throws {
140 | let expiry1: Expiry = .date(Date().addingTimeInterval(-10))
141 | let expiry2: Expiry = .date(Date().addingTimeInterval(10))
142 | let key1 = "key1"
143 | let key2 = "key2"
144 |
145 | try when("save 2 objects with different keys and expiry") {
146 | try storage.setObject(testObject, forKey: key1, expiry: expiry1)
147 | try storage.setObject(testObject, forKey: key2, expiry: expiry2)
148 | }
149 |
150 | try when("remove expired objects") {
151 | try storage.removeExpiredObjects()
152 | }
153 |
154 | then("object with key2 survived") {
155 | XCTAssertNil(try? storage.object(forKey: key1))
156 | XCTAssertNotNil(try? storage.object(forKey: key2))
157 | }
158 | }
159 |
160 | // MARK: - Storage observers
161 |
162 | func testAddStorageObserver() throws {
163 | var changes = [StorageChange]()
164 | storage.addStorageObserver(self) { _, _, change in
165 | changes.append(change)
166 | }
167 |
168 | try storage.setObject(testObject, forKey: "user1")
169 | XCTAssertEqual(changes, [StorageChange.add(key: "user1")])
170 | XCTAssertEqual(storage.storageObservations.count, 1)
171 |
172 | storage.addStorageObserver(self) { _, _, _ in }
173 | XCTAssertEqual(storage.storageObservations.count, 2)
174 | }
175 |
176 | func testRemoveStorageObserver() {
177 | let token = storage.addStorageObserver(self) { _, _, _ in }
178 | XCTAssertEqual(storage.storageObservations.count, 1)
179 |
180 | token.cancel()
181 | XCTAssertTrue(storage.storageObservations.isEmpty)
182 | }
183 |
184 | func testRemoveAllStorageObservers() {
185 | storage.addStorageObserver(self) { _, _, _ in }
186 | storage.addStorageObserver(self) { _, _, _ in }
187 | XCTAssertEqual(storage.storageObservations.count, 2)
188 |
189 | storage.removeAllStorageObservers()
190 | XCTAssertTrue(storage.storageObservations.isEmpty)
191 | }
192 |
193 | // MARK: - Key observers
194 |
195 | func testAddObserverForKey() throws {
196 | var changes = [KeyChange]()
197 | storage.addObserver(self, forKey: "user1") { _, _, change in
198 | changes.append(change)
199 | }
200 |
201 | XCTAssertEqual(storage.keyObservations.count, 1)
202 |
203 | try storage.setObject(testObject, forKey: "user1")
204 | XCTAssertEqual(changes, [KeyChange.edit(before: nil, after: testObject)])
205 |
206 | storage.addObserver(self, forKey: "user1") { _, _, _ in }
207 | XCTAssertEqual(storage.keyObservations.count, 1)
208 |
209 | storage.addObserver(self, forKey: "user2") { _, _, _ in }
210 | XCTAssertEqual(storage.keyObservations.count, 2)
211 | }
212 |
213 | func testRemoveKeyObserver() {
214 | // Test remove for key
215 | storage.addObserver(self, forKey: "user1") { _, _, _ in }
216 | XCTAssertEqual(storage.keyObservations.count, 1)
217 |
218 | storage.removeObserver(forKey: "user1")
219 | XCTAssertTrue(storage.storageObservations.isEmpty)
220 |
221 | // Test remove by token
222 | let token = storage.addObserver(self, forKey: "user2") { _, _, _ in }
223 | XCTAssertEqual(storage.keyObservations.count, 1)
224 |
225 | token.cancel()
226 | XCTAssertTrue(storage.storageObservations.isEmpty)
227 | }
228 |
229 | func testRemoveAllKeyObservers() {
230 | storage.addObserver(self, forKey: "user1") { _, _, _ in }
231 | storage.addObserver(self, forKey: "user2") { _, _, _ in }
232 | XCTAssertEqual(storage.keyObservations.count, 2)
233 |
234 | storage.removeAllKeyObservers()
235 | XCTAssertTrue(storage.keyObservations.isEmpty)
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/Source/Shared/Storage/DiskStorage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Save objects to file on disk
4 | final public class DiskStorage {
5 | enum Error: Swift.Error {
6 | case fileEnumeratorFailed
7 | }
8 |
9 | /// File manager to read/write to the disk
10 | public let fileManager: FileManager
11 | /// Configuration
12 | private let config: DiskConfig
13 | /// The computed path `directory+name`
14 | public let path: String
15 | /// The closure to be called when single file has been removed
16 | var onRemove: ((String) -> Void)?
17 |
18 | private let transformer: Transformer
19 |
20 | // MARK: - Initialization
21 | public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer) throws {
22 | let url: URL
23 | if let directory = config.directory {
24 | url = directory
25 | } else {
26 | url = try fileManager.url(
27 | for: .cachesDirectory,
28 | in: .userDomainMask,
29 | appropriateFor: nil,
30 | create: true
31 | )
32 | }
33 |
34 | // path
35 | let path = url.appendingPathComponent(config.name, isDirectory: true).path
36 |
37 | self.init(config: config, fileManager: fileManager, path: path, transformer: transformer)
38 |
39 | try createDirectory()
40 |
41 | // protection
42 | #if os(iOS) || os(tvOS)
43 | if let protectionType = config.protectionType {
44 | try setDirectoryAttributes([
45 | FileAttributeKey.protectionKey: protectionType
46 | ])
47 | }
48 | #endif
49 | }
50 |
51 | public required init(config: DiskConfig, fileManager: FileManager = FileManager.default, path: String, transformer: Transformer) {
52 | self.config = config
53 | self.fileManager = fileManager
54 | self.path = path
55 | self.transformer = transformer
56 | }
57 | }
58 |
59 | extension DiskStorage: StorageAware {
60 | public func entry(forKey key: String) throws -> Entry {
61 | let filePath = makeFilePath(for: key)
62 | let data = try Data(contentsOf: URL(fileURLWithPath: filePath))
63 | let attributes = try fileManager.attributesOfItem(atPath: filePath)
64 | let object = try transformer.fromData(data)
65 |
66 | guard let date = attributes[.modificationDate] as? Date else {
67 | throw StorageError.malformedFileAttributes
68 | }
69 |
70 | return Entry(
71 | object: object,
72 | expiry: Expiry.date(date),
73 | filePath: filePath
74 | )
75 | }
76 |
77 | public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws {
78 | let expiry = expiry ?? config.expiry
79 | let data = try transformer.toData(object)
80 | let filePath = makeFilePath(for: key)
81 | _ = fileManager.createFile(atPath: filePath, contents: data, attributes: nil)
82 | try fileManager.setAttributes([.modificationDate: expiry.date], ofItemAtPath: filePath)
83 | }
84 |
85 | public func removeObject(forKey key: String) throws {
86 | let filePath = makeFilePath(for: key)
87 | try fileManager.removeItem(atPath: filePath)
88 | onRemove?(filePath)
89 | }
90 |
91 | public func removeAll() throws {
92 | try fileManager.removeItem(atPath: path)
93 | try createDirectory()
94 | }
95 |
96 | public func removeExpiredObjects() throws {
97 | let storageURL = URL(fileURLWithPath: path)
98 | let resourceKeys: [URLResourceKey] = [
99 | .isDirectoryKey,
100 | .contentModificationDateKey,
101 | .totalFileAllocatedSizeKey
102 | ]
103 | var resourceObjects = [ResourceObject]()
104 | var filesToDelete = [URL]()
105 | var totalSize: UInt = 0
106 | let fileEnumerator = fileManager.enumerator(
107 | at: storageURL,
108 | includingPropertiesForKeys: resourceKeys,
109 | options: .skipsHiddenFiles,
110 | errorHandler: nil
111 | )
112 |
113 | guard let urlArray = fileEnumerator?.allObjects as? [URL] else {
114 | throw Error.fileEnumeratorFailed
115 | }
116 |
117 | for url in urlArray {
118 | let resourceValues = try url.resourceValues(forKeys: Set(resourceKeys))
119 | guard resourceValues.isDirectory != true else {
120 | continue
121 | }
122 |
123 | if let expiryDate = resourceValues.contentModificationDate, expiryDate.inThePast {
124 | filesToDelete.append(url)
125 | continue
126 | }
127 |
128 | if let fileSize = resourceValues.totalFileAllocatedSize {
129 | totalSize += UInt(fileSize)
130 | resourceObjects.append((url: url, resourceValues: resourceValues))
131 | }
132 | }
133 |
134 | // Remove expired objects
135 | for url in filesToDelete {
136 | try fileManager.removeItem(at: url)
137 | onRemove?(url.path)
138 | }
139 |
140 | // Remove objects if storage size exceeds max size
141 | try removeResourceObjects(resourceObjects, totalSize: totalSize)
142 | }
143 | }
144 |
145 | extension DiskStorage {
146 | /**
147 | Sets attributes on the disk cache folder.
148 | - Parameter attributes: Directory attributes
149 | */
150 | func setDirectoryAttributes(_ attributes: [FileAttributeKey: Any]) throws {
151 | try fileManager.setAttributes(attributes, ofItemAtPath: path)
152 | }
153 | }
154 |
155 | typealias ResourceObject = (url: Foundation.URL, resourceValues: URLResourceValues)
156 |
157 | extension DiskStorage {
158 | /**
159 | Builds file name from the key.
160 | - Parameter key: Unique key to identify the object in the cache
161 | - Returns: A md5 string
162 | */
163 | func makeFileName(for key: String) -> String {
164 | let fileExtension = URL(fileURLWithPath: key).pathExtension
165 | let fileName = MD5(key)
166 |
167 | switch fileExtension.isEmpty {
168 | case true:
169 | return fileName
170 | case false:
171 | return "\(fileName).\(fileExtension)"
172 | }
173 | }
174 |
175 | /**
176 | Builds file path from the key.
177 | - Parameter key: Unique key to identify the object in the cache
178 | - Returns: A string path based on key
179 | */
180 | func makeFilePath(for key: String) -> String {
181 | return "\(path)/\(makeFileName(for: key))"
182 | }
183 |
184 | /// Calculates total disk cache size.
185 | func totalSize() throws -> UInt64 {
186 | var size: UInt64 = 0
187 | let contents = try fileManager.contentsOfDirectory(atPath: path)
188 | for pathComponent in contents {
189 | let filePath = NSString(string: path).appendingPathComponent(pathComponent)
190 | let attributes = try fileManager.attributesOfItem(atPath: filePath)
191 | if let fileSize = attributes[.size] as? UInt64 {
192 | size += fileSize
193 | }
194 | }
195 | return size
196 | }
197 |
198 | func createDirectory() throws {
199 | guard !fileManager.fileExists(atPath: path) else {
200 | return
201 | }
202 |
203 | try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true,
204 | attributes: nil)
205 | }
206 |
207 | /**
208 | Removes objects if storage size exceeds max size.
209 | - Parameter objects: Resource objects to remove
210 | - Parameter totalSize: Total size
211 | */
212 | func removeResourceObjects(_ objects: [ResourceObject], totalSize: UInt) throws {
213 | guard config.maxSize > 0 && totalSize > config.maxSize else {
214 | return
215 | }
216 |
217 | var totalSize = totalSize
218 | let targetSize = config.maxSize / 2
219 |
220 | let sortedFiles = objects.sorted {
221 | if let time1 = $0.resourceValues.contentModificationDate?.timeIntervalSinceReferenceDate,
222 | let time2 = $1.resourceValues.contentModificationDate?.timeIntervalSinceReferenceDate {
223 | return time1 > time2
224 | } else {
225 | return false
226 | }
227 | }
228 |
229 | for file in sortedFiles {
230 | try fileManager.removeItem(at: file.url)
231 | onRemove?(file.url.path)
232 |
233 | if let fileSize = file.resourceValues.totalFileAllocatedSize {
234 | totalSize -= UInt(fileSize)
235 | }
236 |
237 | if totalSize < targetSize {
238 | break
239 | }
240 | }
241 | }
242 |
243 | /**
244 | Removes the object from the cache if it's expired.
245 | - Parameter key: Unique key to identify the object in the cache
246 | */
247 | func removeObjectIfExpired(forKey key: String) throws {
248 | let filePath = makeFilePath(for: key)
249 | let attributes = try fileManager.attributesOfItem(atPath: filePath)
250 | if let expiryDate = attributes[.modificationDate] as? Date, expiryDate.inThePast {
251 | try fileManager.removeItem(atPath: filePath)
252 | onRemove?(filePath)
253 | }
254 | }
255 | }
256 |
257 | public extension DiskStorage {
258 | func transform(transformer: Transformer) -> DiskStorage {
259 | let storage = DiskStorage(
260 | config: config,
261 | fileManager: fileManager,
262 | path: path,
263 | transformer: transformer
264 | )
265 |
266 | return storage
267 | }
268 | }
269 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## [Unreleased](https://github.com/hyperoslo/Cache/tree/HEAD)
4 |
5 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/2.2.1...HEAD)
6 |
7 | **Closed issues:**
8 |
9 | - Not compatible with swift 3 [\#64](https://github.com/hyperoslo/Cache/issues/64)
10 |
11 | **Merged pull requests:**
12 |
13 | - Fix compile errors in Xcode 8.3 beta [\#74](https://github.com/hyperoslo/Cache/pull/74) ([bradhowes](https://github.com/bradhowes))
14 | - Update Cache.podspec for WatchOS [\#67](https://github.com/hyperoslo/Cache/pull/67) ([FrancoisObob](https://github.com/FrancoisObob))
15 |
16 | ## [2.2.1](https://github.com/hyperoslo/Cache/tree/2.2.1) (2017-01-11)
17 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/2.2.0...2.2.1)
18 |
19 | ## [2.2.0](https://github.com/hyperoslo/Cache/tree/2.2.0) (2017-01-11)
20 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/2.1.1...2.2.0)
21 |
22 | **Closed issues:**
23 |
24 | - Swift 3 support please... again [\#62](https://github.com/hyperoslo/Cache/issues/62)
25 | - Question on threading [\#57](https://github.com/hyperoslo/Cache/issues/57)
26 | - Question - Iterating? [\#51](https://github.com/hyperoslo/Cache/issues/51)
27 | - Question: Completion handlers are called in the original threads [\#40](https://github.com/hyperoslo/Cache/issues/40)
28 |
29 | **Merged pull requests:**
30 |
31 | - Allow manual clear of expired objects [\#65](https://github.com/hyperoslo/Cache/pull/65) ([eofs](https://github.com/eofs))
32 | - Add codecov.io to travis [\#61](https://github.com/hyperoslo/Cache/pull/61) ([zenangst](https://github.com/zenangst))
33 | - Improve build times on Travis [\#60](https://github.com/hyperoslo/Cache/pull/60) ([zenangst](https://github.com/zenangst))
34 |
35 | ## [2.1.1](https://github.com/hyperoslo/Cache/tree/2.1.1) (2016-10-23)
36 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/2.1.0...2.1.1)
37 |
38 | **Closed issues:**
39 |
40 | - Add tvOS target for Carthage [\#55](https://github.com/hyperoslo/Cache/issues/55)
41 |
42 | **Merged pull requests:**
43 |
44 | - Update deployment target [\#59](https://github.com/hyperoslo/Cache/pull/59) ([zenangst](https://github.com/zenangst))
45 |
46 | ## [2.1.0](https://github.com/hyperoslo/Cache/tree/2.1.0) (2016-10-23)
47 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/2.0.0...2.1.0)
48 |
49 | **Merged pull requests:**
50 |
51 | - Feature/tvOS target [\#58](https://github.com/hyperoslo/Cache/pull/58) ([zenangst](https://github.com/zenangst))
52 |
53 | ## [2.0.0](https://github.com/hyperoslo/Cache/tree/2.0.0) (2016-10-13)
54 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/1.5.1...2.0.0)
55 |
56 | **Closed issues:**
57 |
58 | - what time will be support swift3? [\#54](https://github.com/hyperoslo/Cache/issues/54)
59 | - Swift 3 [\#53](https://github.com/hyperoslo/Cache/issues/53)
60 | - 2.3 branch [\#50](https://github.com/hyperoslo/Cache/issues/50)
61 | - Carthage Update error with xcode 8gm build [\#48](https://github.com/hyperoslo/Cache/issues/48)
62 | - Default size of the cache storage [\#38](https://github.com/hyperoslo/Cache/issues/38)
63 |
64 | **Merged pull requests:**
65 |
66 | - Swift 3 [\#56](https://github.com/hyperoslo/Cache/pull/56) ([zenangst](https://github.com/zenangst))
67 | - Added Swift 2.3 support [\#52](https://github.com/hyperoslo/Cache/pull/52) ([Sumolari](https://github.com/Sumolari))
68 | - Removed script which copied the framework - Carthage suggests it should be done at app level. [\#47](https://github.com/hyperoslo/Cache/pull/47) ([okipol88](https://github.com/okipol88))
69 |
70 | ## [1.5.1](https://github.com/hyperoslo/Cache/tree/1.5.1) (2016-08-08)
71 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/1.5.0...1.5.1)
72 |
73 | **Closed issues:**
74 |
75 | - Can integrated with obj-c project? [\#44](https://github.com/hyperoslo/Cache/issues/44)
76 |
77 | **Merged pull requests:**
78 |
79 | - Expose backStorage path in public API [\#46](https://github.com/hyperoslo/Cache/pull/46) ([zenangst](https://github.com/zenangst))
80 |
81 | ## [1.5.0](https://github.com/hyperoslo/Cache/tree/1.5.0) (2016-06-07)
82 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/1.4.0...1.5.0)
83 |
84 | **Merged pull requests:**
85 |
86 | - Feature/config for json serialization [\#43](https://github.com/hyperoslo/Cache/pull/43) ([zenangst](https://github.com/zenangst))
87 |
88 | ## [1.4.0](https://github.com/hyperoslo/Cache/tree/1.4.0) (2016-05-24)
89 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/1.3.0...1.4.0)
90 |
91 | **Closed issues:**
92 |
93 | - Cache with RealmSwift [\#36](https://github.com/hyperoslo/Cache/issues/36)
94 |
95 | **Merged pull requests:**
96 |
97 | - Improve memory handling [\#41](https://github.com/hyperoslo/Cache/pull/41) ([zenangst](https://github.com/zenangst))
98 | - Fix Carthage copy frameworks script [\#39](https://github.com/hyperoslo/Cache/pull/39) ([vadymmarkov](https://github.com/vadymmarkov))
99 |
100 | ## [1.3.0](https://github.com/hyperoslo/Cache/tree/1.3.0) (2016-04-27)
101 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/1.2.0...1.3.0)
102 |
103 | **Closed issues:**
104 |
105 | - i'm hope obtain cache data synchronously [\#33](https://github.com/hyperoslo/Cache/issues/33)
106 |
107 | **Merged pull requests:**
108 |
109 | - Feature: sync cache [\#35](https://github.com/hyperoslo/Cache/pull/35) ([vadymmarkov](https://github.com/vadymmarkov))
110 |
111 | ## [1.2.0](https://github.com/hyperoslo/Cache/tree/1.2.0) (2016-04-26)
112 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/1.1.0...1.2.0)
113 |
114 | **Closed issues:**
115 |
116 | - can you support cache to keychain [\#30](https://github.com/hyperoslo/Cache/issues/30)
117 |
118 | **Merged pull requests:**
119 |
120 | - Bugfix/cannot save file with long URL key [\#34](https://github.com/hyperoslo/Cache/pull/34) ([attila-at-hyper](https://github.com/attila-at-hyper))
121 |
122 | ## [1.1.0](https://github.com/hyperoslo/Cache/tree/1.1.0) (2016-04-24)
123 | [Full Changelog](https://github.com/hyperoslo/Cache/compare/1.0.0...1.1.0)
124 |
125 | **Closed issues:**
126 |
127 | - Question [\#31](https://github.com/hyperoslo/Cache/issues/31)
128 |
129 | **Merged pull requests:**
130 |
131 | - Feature tvOS [\#32](https://github.com/hyperoslo/Cache/pull/32) ([zenangst](https://github.com/zenangst))
132 |
133 | ## [1.0.0](https://github.com/hyperoslo/Cache/tree/1.0.0) (2016-03-29)
134 | **Closed issues:**
135 |
136 | - Add a logo. [\#8](https://github.com/hyperoslo/Cache/issues/8)
137 | - Add descriptive README. [\#3](https://github.com/hyperoslo/Cache/issues/3)
138 | - Pod. [\#2](https://github.com/hyperoslo/Cache/issues/2)
139 | - Missing implementation [\#1](https://github.com/hyperoslo/Cache/issues/1)
140 |
141 | **Merged pull requests:**
142 |
143 | - Fix Expire date issues that screwed up tests on older devices. [\#28](https://github.com/hyperoslo/Cache/pull/28) ([zenangst](https://github.com/zenangst))
144 | - Swift/2.2 [\#27](https://github.com/hyperoslo/Cache/pull/27) ([zenangst](https://github.com/zenangst))
145 | - Remove example [\#25](https://github.com/hyperoslo/Cache/pull/25) ([vadymmarkov](https://github.com/vadymmarkov))
146 | - Remove shared target on CacheDemo [\#24](https://github.com/hyperoslo/Cache/pull/24) ([zenangst](https://github.com/zenangst))
147 | - Disable code signing for Mac target [\#23](https://github.com/hyperoslo/Cache/pull/23) ([zenangst](https://github.com/zenangst))
148 | - Feature: osx support [\#22](https://github.com/hyperoslo/Cache/pull/22) ([vadymmarkov](https://github.com/vadymmarkov))
149 | - Fix image sizes [\#21](https://github.com/hyperoslo/Cache/pull/21) ([vadymmarkov](https://github.com/vadymmarkov))
150 | - Feature: icon [\#20](https://github.com/hyperoslo/Cache/pull/20) ([vadymmarkov](https://github.com/vadymmarkov))
151 | - Feature: README [\#19](https://github.com/hyperoslo/Cache/pull/19) ([vadymmarkov](https://github.com/vadymmarkov))
152 | - Feature: carthage support [\#18](https://github.com/hyperoslo/Cache/pull/18) ([vadymmarkov](https://github.com/vadymmarkov))
153 | - Feature: example [\#17](https://github.com/hyperoslo/Cache/pull/17) ([vadymmarkov](https://github.com/vadymmarkov))
154 | - Fix notifications handling [\#16](https://github.com/hyperoslo/Cache/pull/16) ([vadymmarkov](https://github.com/vadymmarkov))
155 | - Feature: cache clean up [\#15](https://github.com/hyperoslo/Cache/pull/15) ([vadymmarkov](https://github.com/vadymmarkov))
156 | - Feature: default cachable types [\#14](https://github.com/hyperoslo/Cache/pull/14) ([vadymmarkov](https://github.com/vadymmarkov))
157 | - Feature: strict cache + storages [\#13](https://github.com/hyperoslo/Cache/pull/13) ([vadymmarkov](https://github.com/vadymmarkov))
158 | - Feature: cache aggregator [\#12](https://github.com/hyperoslo/Cache/pull/12) ([vadymmarkov](https://github.com/vadymmarkov))
159 | - Expiration date [\#11](https://github.com/hyperoslo/Cache/pull/11) ([vadymmarkov](https://github.com/vadymmarkov))
160 | - Improve read and write [\#10](https://github.com/hyperoslo/Cache/pull/10) ([vadymmarkov](https://github.com/vadymmarkov))
161 | - Remove cache task from protocol [\#9](https://github.com/hyperoslo/Cache/pull/9) ([vadymmarkov](https://github.com/vadymmarkov))
162 | - File cache feature [\#7](https://github.com/hyperoslo/Cache/pull/7) ([vadymmarkov](https://github.com/vadymmarkov))
163 | - Feature/base64 [\#6](https://github.com/hyperoslo/Cache/pull/6) ([vadymmarkov](https://github.com/vadymmarkov))
164 | - Add Cachable protocol [\#5](https://github.com/hyperoslo/Cache/pull/5) ([vadymmarkov](https://github.com/vadymmarkov))
165 | - Cache aware [\#4](https://github.com/hyperoslo/Cache/pull/4) ([vadymmarkov](https://github.com/vadymmarkov))
166 |
167 |
168 |
169 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
--------------------------------------------------------------------------------
/Source/Shared/Library/MD5.swift:
--------------------------------------------------------------------------------
1 | // swiftlint:disable comma function_parameter_count variable_name syntactic_sugar function_body_length vertical_whitespace
2 |
3 | // https://github.com/onmyway133/SwiftHash/blob/master/Sources/MD5.swift
4 |
5 | /*
6 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
7 | * Digest Algorithm, as defined in RFC 1321.
8 | * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
9 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
10 | * Distributed under the BSD License
11 | * See http://pajhome.org.uk/crypt/md5 for more info.
12 | */
13 |
14 | /**
15 | * SwiftHash
16 | * Copyright (c) Khoa Pham 2017
17 | * Licensed under the MIT license. See LICENSE file.
18 | */
19 |
20 | import Foundation
21 |
22 | // MARK: - Public
23 |
24 | public func MD5(_ input: String) -> String {
25 | return hex_md5(input)
26 | }
27 |
28 | // MARK: - Functions
29 |
30 | func hex_md5(_ input: String) -> String {
31 | return rstr2hex(rstr_md5(str2rstr_utf8(input)))
32 | }
33 |
34 | func str2rstr_utf8(_ input: String) -> [CUnsignedChar] {
35 | return Array(input.utf8)
36 | }
37 |
38 | func rstr2tr(_ input: [CUnsignedChar]) -> String {
39 | var output: String = ""
40 |
41 | input.forEach {
42 | output.append(String(UnicodeScalar($0)))
43 | }
44 |
45 | return output
46 | }
47 |
48 | /*
49 | * Convert a raw string to a hex string
50 | */
51 | func rstr2hex(_ input: [CUnsignedChar]) -> String {
52 | let hexTab: [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]
53 | var output: [Character] = []
54 |
55 | for i in 0..> 4) & 0x0F)]
58 | let value2 = hexTab[Int(Int32(x) & 0x0F)]
59 |
60 | output.append(value1)
61 | output.append(value2)
62 | }
63 |
64 | return String(output)
65 | }
66 |
67 | /*
68 | * Convert a raw string to an array of little-endian words
69 | * Characters >255 have their high-byte silently ignored.
70 | */
71 | func rstr2binl(_ input: [CUnsignedChar]) -> [Int32] {
72 | var output: [Int: Int32] = [:]
73 |
74 | for i in stride(from: 0, to: input.count * 8, by: 8) {
75 | let value: Int32 = (Int32(input[i/8]) & 0xFF) << (Int32(i) % 32)
76 |
77 | output[i >> 5] = unwrap(output[i >> 5]) | value
78 | }
79 |
80 | return dictionary2array(output)
81 | }
82 |
83 | /*
84 | * Convert an array of little-endian words to a string
85 | */
86 | func binl2rstr(_ input: [Int32]) -> [CUnsignedChar] {
87 | var output: [CUnsignedChar] = []
88 |
89 | for i in stride(from: 0, to: input.count * 32, by: 8) {
90 | // [i>>5] >>>
91 | let value: Int32 = zeroFillRightShift(input[i>>5], Int32(i % 32)) & 0xFF
92 | output.append(CUnsignedChar(value))
93 | }
94 |
95 | return output
96 | }
97 |
98 | /*
99 | * Calculate the MD5 of a raw string
100 | */
101 | func rstr_md5(_ input: [CUnsignedChar]) -> [CUnsignedChar] {
102 | return binl2rstr(binl_md5(rstr2binl(input), input.count * 8))
103 | }
104 |
105 | /*
106 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally
107 | * to work around bugs in some JS interpreters.
108 | */
109 | func safe_add(_ x: Int32, _ y: Int32) -> Int32 {
110 | let lsw = (x & 0xFFFF) + (y & 0xFFFF)
111 | let msw = (x >> 16) + (y >> 16) + (lsw >> 16)
112 | return (msw << 16) | (lsw & 0xFFFF)
113 | }
114 |
115 | /*
116 | * Bitwise rotate a 32-bit number to the left.
117 | */
118 | func bit_rol(_ num: Int32, _ cnt: Int32) -> Int32 {
119 | // num >>>
120 | return (num << cnt) | zeroFillRightShift(num, (32 - cnt))
121 | }
122 |
123 |
124 | /*
125 | * These funcs implement the four basic operations the algorithm uses.
126 | */
127 | func md5_cmn(_ q: Int32, _ a: Int32, _ b: Int32, _ x: Int32, _ s: Int32, _ t: Int32) -> Int32 {
128 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b)
129 | }
130 |
131 | func md5_ff(_ a: Int32, _ b: Int32, _ c: Int32, _ d: Int32, _ x: Int32, _ s: Int32, _ t: Int32) -> Int32 {
132 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t)
133 | }
134 |
135 | func md5_gg(_ a: Int32, _ b: Int32, _ c: Int32, _ d: Int32, _ x: Int32, _ s: Int32, _ t: Int32) -> Int32 {
136 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t)
137 | }
138 |
139 | func md5_hh(_ a: Int32, _ b: Int32, _ c: Int32, _ d: Int32, _ x: Int32, _ s: Int32, _ t: Int32) -> Int32 {
140 | return md5_cmn(b ^ c ^ d, a, b, x, s, t)
141 | }
142 |
143 | func md5_ii(_ a: Int32, _ b: Int32, _ c: Int32, _ d: Int32, _ x: Int32, _ s: Int32, _ t: Int32) -> Int32 {
144 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t)
145 | }
146 |
147 |
148 | /*
149 | * Calculate the MD5 of an array of little-endian words, and a bit length.
150 | */
151 | func binl_md5(_ input: [Int32], _ len: Int) -> [Int32] {
152 | /* append padding */
153 |
154 | var x: [Int: Int32] = [:]
155 | for (index, value) in input.enumerated() {
156 | x[index] = value
157 | }
158 |
159 | let value: Int32 = 0x80 << Int32((len) % 32)
160 | x[len >> 5] = unwrap(x[len >> 5]) | value
161 |
162 | // >>> 9
163 | let index = (((len + 64) >> 9) << 4) + 14
164 | x[index] = unwrap(x[index]) | Int32(len)
165 |
166 | var a: Int32 = 1732584193
167 | var b: Int32 = -271733879
168 | var c: Int32 = -1732584194
169 | var d: Int32 = 271733878
170 |
171 | for i in stride(from: 0, to: length(x), by: 16) {
172 | let olda: Int32 = a
173 | let oldb: Int32 = b
174 | let oldc: Int32 = c
175 | let oldd: Int32 = d
176 |
177 | a = md5_ff(a, b, c, d, unwrap(x[i + 0]), 7 , -680876936)
178 | d = md5_ff(d, a, b, c, unwrap(x[i + 1]), 12, -389564586)
179 | c = md5_ff(c, d, a, b, unwrap(x[i + 2]), 17, 606105819)
180 | b = md5_ff(b, c, d, a, unwrap(x[i + 3]), 22, -1044525330)
181 | a = md5_ff(a, b, c, d, unwrap(x[i + 4]), 7 , -176418897)
182 | d = md5_ff(d, a, b, c, unwrap(x[i + 5]), 12, 1200080426)
183 | c = md5_ff(c, d, a, b, unwrap(x[i + 6]), 17, -1473231341)
184 | b = md5_ff(b, c, d, a, unwrap(x[i + 7]), 22, -45705983)
185 | a = md5_ff(a, b, c, d, unwrap(x[i + 8]), 7 , 1770035416)
186 | d = md5_ff(d, a, b, c, unwrap(x[i + 9]), 12, -1958414417)
187 | c = md5_ff(c, d, a, b, unwrap(x[i + 10]), 17, -42063)
188 | b = md5_ff(b, c, d, a, unwrap(x[i + 11]), 22, -1990404162)
189 | a = md5_ff(a, b, c, d, unwrap(x[i + 12]), 7 , 1804603682)
190 | d = md5_ff(d, a, b, c, unwrap(x[i + 13]), 12, -40341101)
191 | c = md5_ff(c, d, a, b, unwrap(x[i + 14]), 17, -1502002290)
192 | b = md5_ff(b, c, d, a, unwrap(x[i + 15]), 22, 1236535329)
193 |
194 | a = md5_gg(a, b, c, d, unwrap(x[i + 1]), 5 , -165796510)
195 | d = md5_gg(d, a, b, c, unwrap(x[i + 6]), 9 , -1069501632)
196 | c = md5_gg(c, d, a, b, unwrap(x[i + 11]), 14, 643717713)
197 | b = md5_gg(b, c, d, a, unwrap(x[i + 0]), 20, -373897302)
198 | a = md5_gg(a, b, c, d, unwrap(x[i + 5]), 5 , -701558691)
199 | d = md5_gg(d, a, b, c, unwrap(x[i + 10]), 9 , 38016083)
200 | c = md5_gg(c, d, a, b, unwrap(x[i + 15]), 14, -660478335)
201 | b = md5_gg(b, c, d, a, unwrap(x[i + 4]), 20, -405537848)
202 | a = md5_gg(a, b, c, d, unwrap(x[i + 9]), 5 , 568446438)
203 | d = md5_gg(d, a, b, c, unwrap(x[i + 14]), 9 , -1019803690)
204 | c = md5_gg(c, d, a, b, unwrap(x[i + 3]), 14, -187363961)
205 | b = md5_gg(b, c, d, a, unwrap(x[i + 8]), 20, 1163531501)
206 | a = md5_gg(a, b, c, d, unwrap(x[i + 13]), 5 , -1444681467)
207 | d = md5_gg(d, a, b, c, unwrap(x[i + 2]), 9 , -51403784)
208 | c = md5_gg(c, d, a, b, unwrap(x[i + 7]), 14, 1735328473)
209 | b = md5_gg(b, c, d, a, unwrap(x[i + 12]), 20, -1926607734)
210 |
211 | a = md5_hh(a, b, c, d, unwrap(x[i + 5]), 4 , -378558)
212 | d = md5_hh(d, a, b, c, unwrap(x[i + 8]), 11, -2022574463)
213 | c = md5_hh(c, d, a, b, unwrap(x[i + 11]), 16, 1839030562)
214 | b = md5_hh(b, c, d, a, unwrap(x[i + 14]), 23, -35309556)
215 | a = md5_hh(a, b, c, d, unwrap(x[i + 1]), 4 , -1530992060)
216 | d = md5_hh(d, a, b, c, unwrap(x[i + 4]), 11, 1272893353)
217 | c = md5_hh(c, d, a, b, unwrap(x[i + 7]), 16, -155497632)
218 | b = md5_hh(b, c, d, a, unwrap(x[i + 10]), 23, -1094730640)
219 | a = md5_hh(a, b, c, d, unwrap(x[i + 13]), 4 , 681279174)
220 | d = md5_hh(d, a, b, c, unwrap(x[i + 0]), 11, -358537222)
221 | c = md5_hh(c, d, a, b, unwrap(x[i + 3]), 16, -722521979)
222 | b = md5_hh(b, c, d, a, unwrap(x[i + 6]), 23, 76029189)
223 | a = md5_hh(a, b, c, d, unwrap(x[i + 9]), 4 , -640364487)
224 | d = md5_hh(d, a, b, c, unwrap(x[i + 12]), 11, -421815835)
225 | c = md5_hh(c, d, a, b, unwrap(x[i + 15]), 16, 530742520)
226 | b = md5_hh(b, c, d, a, unwrap(x[i + 2]), 23, -995338651)
227 |
228 | a = md5_ii(a, b, c, d, unwrap(x[i + 0]), 6 , -198630844)
229 | d = md5_ii(d, a, b, c, unwrap(x[i + 7]), 10, 1126891415)
230 | c = md5_ii(c, d, a, b, unwrap(x[i + 14]), 15, -1416354905)
231 | b = md5_ii(b, c, d, a, unwrap(x[i + 5]), 21, -57434055)
232 | a = md5_ii(a, b, c, d, unwrap(x[i + 12]), 6 , 1700485571)
233 | d = md5_ii(d, a, b, c, unwrap(x[i + 3]), 10, -1894986606)
234 | c = md5_ii(c, d, a, b, unwrap(x[i + 10]), 15, -1051523)
235 | b = md5_ii(b, c, d, a, unwrap(x[i + 1]), 21, -2054922799)
236 | a = md5_ii(a, b, c, d, unwrap(x[i + 8]), 6 , 1873313359)
237 | d = md5_ii(d, a, b, c, unwrap(x[i + 15]), 10, -30611744)
238 | c = md5_ii(c, d, a, b, unwrap(x[i + 6]), 15, -1560198380)
239 | b = md5_ii(b, c, d, a, unwrap(x[i + 13]), 21, 1309151649)
240 | a = md5_ii(a, b, c, d, unwrap(x[i + 4]), 6 , -145523070)
241 | d = md5_ii(d, a, b, c, unwrap(x[i + 11]), 10, -1120210379)
242 | c = md5_ii(c, d, a, b, unwrap(x[i + 2]), 15, 718787259)
243 | b = md5_ii(b, c, d, a, unwrap(x[i + 9]), 21, -343485551)
244 |
245 | a = safe_add(a, olda)
246 | b = safe_add(b, oldb)
247 | c = safe_add(c, oldc)
248 | d = safe_add(d, oldd)
249 | }
250 |
251 | return [a, b, c, d]
252 | }
253 |
254 | // MARK: - Helper
255 |
256 | func length(_ dictionary: [Int: Int32]) -> Int {
257 | return (dictionary.keys.max() ?? 0) + 1
258 | }
259 |
260 | func dictionary2array(_ dictionary: [Int: Int32]) -> [Int32] {
261 | var array = Array(repeating: 0, count: dictionary.keys.count)
262 |
263 | for i in Array(dictionary.keys).sorted() {
264 | array[i] = unwrap(dictionary[i])
265 | }
266 |
267 | return array
268 | }
269 |
270 | func unwrap(_ value: Int32?, _ fallback: Int32 = 0) -> Int32 {
271 | if let value = value {
272 | return value
273 | }
274 |
275 | return fallback
276 | }
277 |
278 | func zeroFillRightShift(_ num: Int32, _ count: Int32) -> Int32 {
279 | let value = UInt32(bitPattern: num) >> UInt32(bitPattern: count)
280 | return Int32(bitPattern: value)
281 | }
282 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### This library has been deprecated and the repo has been archived.
2 | ### Use the original upstream at [hyperoslo/Cache](https://github.com/hyperoslo/Cache) instead.
3 |
4 | 
5 |
6 | [](https://circleci.com/gh/hyperoslo/Cache)
7 | [](http://cocoadocs.org/docsets/Cache)
8 | [](https://github.com/Carthage/Carthage)
9 | [](http://cocoadocs.org/docsets/Cache)
10 | [](http://cocoadocs.org/docsets/Cache)
11 | [](http://cocoadocs.org/docsets/Cache)
12 | 
13 |
14 | ## Table of Contents
15 |
16 | * [Description](#description)
17 | * [Key features](#key-features)
18 | * [Usage](#usage)
19 | * [Storage](#storage)
20 | * [Configuration](#configuration)
21 | * [Sync APIs](#sync-apis)
22 | * [Async APIs](#async-apis)
23 | * [Expiry date](#expiry-date)
24 | * [Observations](#observations)
25 | * [Storage observations](#storage-observations)
26 | * [Key observations](#key-observations)
27 | * [Handling JSON response](#handling-json-response)
28 | * [What about images?](#what-about-images)
29 | * [Installation](#installation)
30 | * [Author](#author)
31 | * [Contributing](#contributing)
32 | * [License](#license)
33 |
34 | ## Description
35 |
36 |
37 |
38 | **Cache** doesn't claim to be unique in this area, but it's not another monster
39 | library that gives you a god's power. It does nothing but caching, but it does it well. It offers a good public API
40 | with out-of-box implementations and great customization possibilities. `Cache` utilizes `Codable` in Swift 4 to perform serialization.
41 |
42 | Read the story here [Open Source Stories: From Cachable to Generic Storage in Cache](https://medium.com/hyperoslo/open-source-stories-from-cachable-to-generic-storage-in-cache-418d9a230d51)
43 |
44 | ## Key features
45 |
46 | - [x] Work with Swift 4 `Codable`. Anything conforming to `Codable` will be saved and loaded easily by `Storage`.
47 | - [x] Hybrid with memory and disk storage.
48 | - [X] Many options via `DiskConfig` and `MemoryConfig`.
49 | - [x] Support `expiry` and clean up of expired objects.
50 | - [x] Thread safe. Operations can be accessed from any queue.
51 | - [x] Sync by default. Also support Async APIs.
52 | - [x] Extensive unit test coverage and great documentation.
53 | - [x] iOS, tvOS and macOS support.
54 |
55 | ## Usage
56 |
57 | ### Storage
58 |
59 | `Cache` is built based on [Chain-of-responsibility pattern](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern), in which there are many processing objects, each knows how to do 1 task and delegates to the next one, so can you compose Storages the way you like.
60 |
61 | For now the following Storage are supported
62 |
63 | - `MemoryStorage`: save object to memory.
64 | - `DiskStorage`: save object to disk.
65 | - `HybridStorage`: save object to memory and disk, so you get persistented object on disk, while fast access with in memory objects.
66 | - `SyncStorage`: blocking APIs, all read and write operations are scheduled in a serial queue, all sync manner.
67 | - `AsyncStorage`: non-blocking APIs, operations are scheduled in an internal queue for serial processing. No read and write should happen at the same time.
68 |
69 | Although you can use those Storage at your discretion, you don't have to. Because we also provide a convenient `Storage` which uses `HybridStorage` under the hood, while exposes sync and async APIs through `SyncStorage` and `AsyncStorage`.
70 |
71 | All you need to do is to specify the configuration you want with `DiskConfig` and `MemoryConfig`. The default configurations are good to go, but you can customise a lot.
72 |
73 |
74 | ```swift
75 | let diskConfig = DiskConfig(name: "Floppy")
76 | let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)
77 |
78 | let storage = try? Storage(
79 | diskConfig: diskConfig,
80 | memoryConfig: memoryConfig,
81 | transformer: TransformerFactory.forCodable(ofType: User.self) // Storage
82 | )
83 | ```
84 |
85 | ### Generic, Type safety and Transformer
86 |
87 | All `Storage` now are generic by default, so you can get a type safety experience. Once you create a Storage, it has a type constraint that you don't need to specify type for each operation afterwards.
88 |
89 | If you want to change the type, `Cache` offers `transform` functions, look for `Transformer` and `TransformerFactory` for built-in transformers.
90 |
91 | ```swift
92 | let storage: Storage = ...
93 | storage.setObject(superman, forKey: "user")
94 |
95 | let imageStorage = storage.transformImage() // Storage
96 | imageStorage.setObject(image, forKey: "image")
97 |
98 | let stringStorage = storage.transformCodable(ofType: String.self) // Storage
99 | stringStorage.setObject("hello world", forKey: "string")
100 | ```
101 |
102 | Each transformation allows you to work with a specific type, however the underlying caching mechanism remains the same, you're working with the same `Storage`, just with different type annotation. You can also create different `Storage` for each type if you want.
103 |
104 | `Transformer` is necessary because the need of serialising and deserialising objects to and from `Data` for disk persistency. `Cache` provides default `Transformer ` for `Data`, `Codable` and `UIImage/NSImage`
105 |
106 | #### Codable types
107 |
108 | `Storage` supports any objects that conform to [Codable](https://developer.apple.com/documentation/swift/codable) protocol. You can [make your own things conform to Codable](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) so that can be saved and loaded from `Storage`.
109 |
110 | The supported types are
111 |
112 | - Primitives like `Int`, `Float`, `String`, `Bool`, ...
113 | - Array of primitives like `[Int]`, `[Float]`, `[Double]`, ...
114 | - Set of primitives like `Set`, `Set`, ...
115 | - Simply dictionary like `[String: Int]`, `[String: String]`, ...
116 | - `Date`
117 | - `URL`
118 | - `Data`
119 |
120 | #### Error handling
121 |
122 | Error handling is done via `try catch`. `Storage` throws errors in terms of `StorageError`.
123 |
124 | ```swift
125 | public enum StorageError: Error {
126 | /// Object can not be found
127 | case notFound
128 | /// Object is found, but casting to requested type failed
129 | case typeNotMatch
130 | /// The file attributes are malformed
131 | case malformedFileAttributes
132 | /// Can't perform Decode
133 | case decodingFailed
134 | /// Can't perform Encode
135 | case encodingFailed
136 | /// The storage has been deallocated
137 | case deallocated
138 | /// Fail to perform transformation to or from Data
139 | case transformerFail
140 | }
141 | ```
142 |
143 | There can be errors because of disk problem or type mismatch when loading from storage, so if want to handle errors, you need to do `try catch`
144 |
145 | ```swift
146 | do {
147 | let storage = try Storage(diskConfig: diskConfig, memoryConfig: memoryConfig)
148 | } catch {
149 | print(error)
150 | }
151 | ```
152 |
153 | ### Configuration
154 |
155 | Here is how you can play with many configuration options
156 |
157 | ```swift
158 | let diskConfig = DiskConfig(
159 | // The name of disk storage, this will be used as folder name within directory
160 | name: "Floppy",
161 | // Expiry date that will be applied by default for every added object
162 | // if it's not overridden in the `setObject(forKey:expiry:)` method
163 | expiry: .date(Date().addingTimeInterval(2*3600)),
164 | // Maximum size of the disk cache storage (in bytes)
165 | maxSize: 10000,
166 | // Where to store the disk cache. If nil, it is placed in `cachesDirectory` directory.
167 | directory: try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask,
168 | appropriateFor: nil, create: true).appendingPathComponent("MyPreferences"),
169 | // Data protection is used to store files in an encrypted format on disk and to decrypt them on demand
170 | protectionType: .complete
171 | )
172 | ```
173 |
174 | ```swift
175 | let memoryConfig = MemoryConfig(
176 | // Expiry date that will be applied by default for every added object
177 | // if it's not overridden in the `setObject(forKey:expiry:)` method
178 | expiry: .date(Date().addingTimeInterval(2*60)),
179 | /// The maximum number of objects in memory the cache should hold
180 | countLimit: 50,
181 | /// The maximum total cost that the cache can hold before it starts evicting objects
182 | totalCostLimit: 0
183 | )
184 | ```
185 |
186 | On iOS, tvOS we can also specify `protectionType` on `DiskConfig` to add a level of security to files stored on disk by your app in the app’s container. For more information, see [FileProtectionType](https://developer.apple.com/documentation/foundation/fileprotectiontype)
187 |
188 | ### Sync APIs
189 |
190 | `Storage` is sync by default and is `thread safe`, you can access it from any queues. All Sync functions are constrained by `StorageAware` protocol.
191 |
192 | ```swift
193 | // Save to storage
194 | try? storage.setObject(10, forKey: "score")
195 | try? storage.setObject("Oslo", forKey: "my favorite city", expiry: .never)
196 | try? storage.setObject(["alert", "sounds", "badge"], forKey: "notifications")
197 | try? storage.setObject(data, forKey: "a bunch of bytes")
198 | try? storage.setObject(authorizeURL, forKey: "authorization URL")
199 |
200 | // Load from storage
201 | let score = try? storage.object(forKey: "score")
202 | let favoriteCharacter = try? storage.object(forKey: "my favorite city")
203 |
204 | // Check if an object exists
205 | let hasFavoriteCharacter = try? storage.existsObject(forKey: "my favorite city")
206 |
207 | // Remove an object in storage
208 | try? storage.removeObject(forKey: "my favorite city")
209 |
210 | // Remove all objects
211 | try? storage.removeAll()
212 |
213 | // Remove expired objects
214 | try? storage.removeExpiredObjects()
215 | ```
216 |
217 | #### Entry
218 |
219 | There is time you want to get object together with its expiry information and meta data. You can use `Entry`
220 |
221 | ```swift
222 | let entry = try? storage.entry(forKey: "my favorite city")
223 | print(entry?.object)
224 | print(entry?.expiry)
225 | print(entry?.meta)
226 | ```
227 |
228 | `meta` may contain file information if the object was fetched from disk storage.
229 |
230 | #### Custom Codable
231 |
232 | `Codable` works for simple dictionary like `[String: Int]`, `[String: String]`, ... It does not work for `[String: Any]` as `Any` is not `Codable` conformance, it will raise `fatal error` at runtime. So when you get json from backend responses, you need to convert that to your custom `Codable` objects and save to `Storage` instead.
233 |
234 | ```swift
235 | struct User: Codable {
236 | let firstName: String
237 | let lastName: String
238 | }
239 |
240 | let user = User(fistName: "John", lastName: "Snow")
241 | try? storage.setObject(user, forKey: "character")
242 | ```
243 |
244 | ### Async APIs
245 |
246 | In `async` fashion, you deal with `Result` instead of `try catch` because the result is delivered at a later time, in order to not block the current calling queue. In the completion block, you either have `value` or `error`.
247 |
248 | You access Async APIs via `storage.async`, it is also thread safe, and you can use Sync and Async APIs in any order you want. All Async functions are constrained by `AsyncStorageAware` protocol.
249 |
250 | ```swift
251 | storage.async.setObject("Oslo", forKey: "my favorite city") { result in
252 | switch result {
253 | case .value:
254 | print("saved successfully")
255 | case .error(let error):
256 | print(error)
257 | }
258 | }
259 |
260 | storage.async.object(forKey: "my favorite city") { result in
261 | switch result {
262 | case .value(let city):
263 | print("my favorite city is \(city)")
264 | case .error(let error):
265 | print(error)
266 | }
267 | }
268 |
269 | storage.async.existsObject(forKey: "my favorite city") { result in
270 | if case .value(let exists) = result, exists {
271 | print("I have a favorite city")
272 | }
273 | }
274 |
275 | storage.async.removeAll() { result in
276 | switch result {
277 | case .value:
278 | print("removal completes")
279 | case .error(let error):
280 | print(error)
281 | }
282 | }
283 |
284 | storage.async.removeExpiredObjects() { result in
285 | switch result {
286 | case .value:
287 | print("removal completes")
288 | case .error(let error):
289 | print(error)
290 | }
291 | }
292 | ```
293 |
294 | ### Expiry date
295 |
296 | By default, all saved objects have the same expiry as the expiry you specify in `DiskConfig` or `MemoryConfig`. You can overwrite this for a specific object by specifying `expiry` for `setObject`
297 |
298 | ```swift
299 | // Default expiry date from configuration will be applied to the item
300 | try? storage.setObject("This is a string", forKey: "string")
301 |
302 | // A given expiry date will be applied to the item
303 | try? storage.setObject(
304 | "This is a string",
305 | forKey: "string"
306 | expiry: .date(Date().addingTimeInterval(2 * 3600))
307 | )
308 |
309 | // Clear expired objects
310 | storage.removeExpiredObjects()
311 | ```
312 |
313 | ## Observations
314 |
315 | [Storage](#storage) allows you to observe changes in the cache layer, both on
316 | a store and a key levels. The API lets you pass any object as an observer,
317 | while also passing an observation closure. The observation closure will be
318 | removed automatically when the weakly captured observer has been deallocated.
319 |
320 | ## Storage observations
321 |
322 | ```swift
323 | // Add observer
324 | let token = storage.addStorageObserver(self) { observer, storage, change in
325 | switch change {
326 | case .add(let key):
327 | print("Added \(key)")
328 | case .remove(let key):
329 | print("Removed \(key)")
330 | case .removeAll:
331 | print("Removed all")
332 | case .removeExpired:
333 | print("Removed expired")
334 | }
335 | }
336 |
337 | // Remove observer
338 | token.cancel()
339 |
340 | // Remove all observers
341 | storage.removeAllStorageObservers()
342 | ```
343 |
344 | ## Key observations
345 |
346 | ```swift
347 | let key = "user1"
348 |
349 | let token = storage.addObserver(self, forKey: key) { observer, storage, change in
350 | switch change {
351 | case .edit(let before, let after):
352 | print("Changed object for \(key) from \(String(describing: before)) to \(after)")
353 | case .remove:
354 | print("Removed \(key)")
355 | }
356 | }
357 |
358 | // Remove observer by token
359 | token.cancel()
360 |
361 | // Remove observer for key
362 | storage.removeObserver(forKey: key)
363 |
364 | // Remove all observers
365 | storage.removeAllKeyObservers()
366 | ```
367 |
368 | ## Handling JSON response
369 |
370 | Most of the time, our use case is to fetch some json from backend, display it while saving the json to storage for future uses. If you're using libraries like [Alamofire](https://github.com/Alamofire/Alamofire) or [Malibu](https://github.com/hyperoslo/Malibu), you mostly get json in the form of dictionary, string, or data.
371 |
372 | `Storage` can persist `String` or `Data`. You can even save json to `Storage` using `JSONArrayWrapper` and `JSONDictionaryWrapper`, but we prefer persisting the strong typed objects, since those are the objects that you will use to display in UI. Furthermore, if the json data can't be converted to strongly typed objects, what's the point of saving it ? 😉
373 |
374 | You can use these extensions on `JSONDecoder` to decode json dictionary, string or data to objects.
375 |
376 | ```swift
377 | let user = JSONDecoder.decode(jsonString, to: User.self)
378 | let cities = JSONDecoder.decode(jsonDictionary, to: [City].self)
379 | let dragons = JSONDecoder.decode(jsonData, to: [Dragon].self)
380 | ```
381 |
382 | This is how you perform object converting and saving with `Alamofire`
383 |
384 | ```swift
385 | Alamofire.request("https://gameofthrones.org/mostFavoriteCharacter").responseString { response in
386 | do {
387 | let user = try JSONDecoder.decode(response.result.value, to: User.self)
388 | try storage.setObject(user, forKey: "most favorite character")
389 | } catch {
390 | print(error)
391 | }
392 | }
393 | ```
394 |
395 | ## What about images
396 |
397 | If you want to load image into `UIImageView` or `NSImageView`, then we also have a nice gift for you. It's called [Imaginary](https://github.com/hyperoslo/Imaginary) and uses `Cache` under the hood to make you life easier when it comes to working with remote images.
398 |
399 | ## Installation
400 |
401 | ### Cocoapods
402 |
403 | **Cache** is available through [CocoaPods](http://cocoapods.org). To install
404 | it, simply add the following line to your Podfile:
405 |
406 | ```ruby
407 | pod 'Cache'
408 | ```
409 |
410 | ### Carthage
411 |
412 | **Cache** is also available through [Carthage](https://github.com/Carthage/Carthage).
413 | To install just write into your Cartfile:
414 |
415 | ```ruby
416 | github "hyperoslo/Cache"
417 | ```
418 |
419 | You also need to add `SwiftHash.framework` in your [copy-frameworks](https://github.com/Carthage/Carthage#if-youre-building-for-ios-tvos-or-watchos) script.
420 |
421 | ## Author
422 |
423 | - [Hyper](http://hyper.no) made this with ❤️
424 | - Inline MD5 implementation from [SwiftHash](https://github.com/onmyway133/SwiftHash)
425 |
426 | ## Contributing
427 |
428 | We would love you to contribute to **Cache**, check the [CONTRIBUTING](https://github.com/hyperoslo/Cache/blob/master/CONTRIBUTING.md) file for more info.
429 |
430 | ## License
431 |
432 | **Cache** is available under the MIT license. See the [LICENSE](https://github.com/hyperoslo/Cache/blob/master/LICENSE.md) file for more info.
433 |
--------------------------------------------------------------------------------