Skip to content

Commit dd5cc36

Browse files
authored
Merge pull request #267 from hyperoslo/generic-key
Generic key
2 parents 79bfb12 + 98c6400 commit dd5cc36

28 files changed

+234
-145
lines changed

Cache.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
0E79164A250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E791649250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift */; };
11+
0E79164B250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E791649250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift */; };
12+
0E79164C250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E791649250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift */; };
13+
0E79164E250E2B0400A71666 /* Hasher+constantAccrossExecutions+Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E79164D250E2B0400A71666 /* Hasher+constantAccrossExecutions+Tests.swift */; };
1014
BDEDD37D1DBCEB8A007416A6 /* Cache.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BDEDD3561DBCE5B1007416A6 /* Cache.framework */; };
1115
D21B667C1F6A723C00125DE1 /* DataSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF98531F694FFA00CE8F68 /* DataSerializer.swift */; };
1216
D21B667E1F6A723C00125DE1 /* ExpirationMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF98551F694FFA00CE8F68 /* ExpirationMode.swift */; };
@@ -173,6 +177,8 @@
173177
/* End PBXContainerItemProxy section */
174178

175179
/* Begin PBXFileReference section */
180+
0E791649250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Hasher+constantAccrossExecutions.swift"; sourceTree = "<group>"; };
181+
0E79164D250E2B0400A71666 /* Hasher+constantAccrossExecutions+Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Hasher+constantAccrossExecutions+Tests.swift"; sourceTree = "<group>"; };
176182
BDEDD3561DBCE5B1007416A6 /* Cache.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Cache.framework; sourceTree = BUILT_PRODUCTS_DIR; };
177183
BDEDD3781DBCEB8A007416A6 /* Cache-tvOS-Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Cache-tvOS-Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
178184
D221E5BB20D00D9300BC940E /* MemoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryStorage.swift; sourceTree = "<group>"; };
@@ -323,6 +329,7 @@
323329
D2CF984F1F694FFA00CE8F68 /* Extensions */ = {
324330
isa = PBXGroup;
325331
children = (
332+
0E791649250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift */,
326333
D28897041F8B79B300C61DEE /* JSONDecoder+Extensions.swift */,
327334
D2CF98501F694FFA00CE8F68 /* Date+Extensions.swift */,
328335
);
@@ -384,6 +391,7 @@
384391
isa = PBXGroup;
385392
children = (
386393
D2CF98721F69513800CE8F68 /* Date+ExtensionsTests.swift */,
394+
0E79164D250E2B0400A71666 /* Hasher+constantAccrossExecutions+Tests.swift */,
387395
);
388396
path = Extensions;
389397
sourceTree = "<group>";
@@ -803,6 +811,7 @@
803811
D270147E20D107DA003B45C7 /* SyncStorage.swift in Sources */,
804812
D292DAFF1F6A970B0060F614 /* Result.swift in Sources */,
805813
D21B669A1F6A724300125DE1 /* Date+Extensions.swift in Sources */,
814+
0E79164C250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */,
806815
D21B66891F6A723C00125DE1 /* ImageWrapper.swift in Sources */,
807816
D21B668B1F6A723C00125DE1 /* StorageError.swift in Sources */,
808817
D5A9D1B921134547005DBD3F /* ObservationToken.swift in Sources */,
@@ -848,6 +857,7 @@
848857
D2CF987C1F69513800CE8F68 /* Date+ExtensionsTests.swift in Sources */,
849858
D28C9BAC1F67ECD400C180C1 /* TestHelper+iOS.swift in Sources */,
850859
D2CF98211F69427C00CE8F68 /* TestHelper.swift in Sources */,
860+
0E79164E250E2B0400A71666 /* Hasher+constantAccrossExecutions+Tests.swift in Sources */,
851861
D511464F21147B7C00197DCE /* ObservationTokenTests.swift in Sources */,
852862
D2CF987F1F69513800CE8F68 /* ImageWrapperTests.swift in Sources */,
853863
D2D4CC1A1FA3166900E4A2D5 /* MD5Tests.swift in Sources */,
@@ -889,6 +899,7 @@
889899
D270147D20D107DA003B45C7 /* SyncStorage.swift in Sources */,
890900
D292DAFE1F6A970B0060F614 /* Result.swift in Sources */,
891901
D21B66991F6A724200125DE1 /* Date+Extensions.swift in Sources */,
902+
0E79164B250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */,
892903
D21B66801F6A723C00125DE1 /* ImageWrapper.swift in Sources */,
893904
D21B66821F6A723C00125DE1 /* StorageError.swift in Sources */,
894905
D5A9D1B821134547005DBD3F /* ObservationToken.swift in Sources */,
@@ -942,6 +953,7 @@
942953
D2D4CC241FA3426B00E4A2D5 /* JSONArrayWrapper.swift in Sources */,
943954
D270147C20D107DA003B45C7 /* SyncStorage.swift in Sources */,
944955
D2CF98671F694FFA00CE8F68 /* Expiry.swift in Sources */,
956+
0E79164A250E2AA500A71666 /* Hasher+constantAccrossExecutions.swift in Sources */,
945957
D270148820D11040003B45C7 /* Storage+Transform.swift in Sources */,
946958
D2CF986A1F694FFA00CE8F68 /* StorageError.swift in Sources */,
947959
D5A9D1B721134547005DBD3F /* ObservationToken.swift in Sources */,
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
import Foundation
3+
4+
extension Hasher {
5+
// Stolen from https://github.com/apple/swift/blob/master/stdlib/public/core/SipHash.swift
6+
// in order to replicate the exact format in bytes
7+
private struct _State {
8+
private var v0: UInt64 = 0x736f6d6570736575
9+
private var v1: UInt64 = 0x646f72616e646f6d
10+
private var v2: UInt64 = 0x6c7967656e657261
11+
private var v3: UInt64 = 0x7465646279746573
12+
private var v4: UInt64 = 0
13+
private var v5: UInt64 = 0
14+
private var v6: UInt64 = 0
15+
private var v7: UInt64 = 0
16+
}
17+
18+
static func constantAccrossExecutions() -> Hasher {
19+
let offset = MemoryLayout<Hasher>.size - MemoryLayout<_State>.size
20+
var hasher = Hasher()
21+
withUnsafeMutableBytes(of: &hasher) { pointer in
22+
pointer.baseAddress!.storeBytes(of: _State(), toByteOffset: offset, as: _State.self)
23+
}
24+
return hasher
25+
}
26+
}

Source/Shared/Library/ImageWrapper.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22

3+
#if os(iOS) || os(tvOS) || os(macOS)
34
public struct ImageWrapper: Codable {
45
public let image: Image
56

@@ -30,3 +31,4 @@ public struct ImageWrapper: Codable {
3031
try container.encode(data, forKey: CodingKeys.image)
3132
}
3233
}
34+
#endif

Source/Shared/Library/TransformerFactory.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class TransformerFactory {
99
return Transformer<Data>(toData: toData, fromData: fromData)
1010
}
1111

12+
#if os(iOS) || os(tvOS) || os(macOS)
1213
public static func forImage() -> Transformer<Image> {
1314
let toData: (Image) throws -> Data = { image in
1415
return try image.cache_toData().unwrapOrThrow(error: StorageError.transformerFail)
@@ -20,6 +21,7 @@ public class TransformerFactory {
2021

2122
return Transformer<Image>(toData: toData, fromData: fromData)
2223
}
24+
#endif
2325

2426
public static func forCodable<U: Codable>(ofType: U.Type) -> Transformer<U> {
2527
let toData: (U) throws -> Data = { object in

Source/Shared/Storage/AsyncStorage.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ import Dispatch
33

44
/// Manipulate storage in a "all async" manner.
55
/// The completion closure will be called when operation completes.
6-
public class AsyncStorage<T> {
7-
public let innerStorage: HybridStorage<T>
6+
public class AsyncStorage<Key: Hashable, Value> {
7+
public let innerStorage: HybridStorage<Key, Value>
88
public let serialQueue: DispatchQueue
99

10-
public init(storage: HybridStorage<T>, serialQueue: DispatchQueue) {
10+
public init(storage: HybridStorage<Key, Value>, serialQueue: DispatchQueue) {
1111
self.innerStorage = storage
1212
self.serialQueue = serialQueue
1313
}
1414
}
1515

1616
extension AsyncStorage {
17-
public func entry(forKey key: String, completion: @escaping (Result<Entry<T>>) -> Void) {
17+
public func entry(forKey key: Key, completion: @escaping (Result<Entry<Value>>) -> Void) {
1818
serialQueue.async { [weak self] in
1919
guard let `self` = self else {
2020
completion(Result.error(StorageError.deallocated))
@@ -30,7 +30,7 @@ extension AsyncStorage {
3030
}
3131
}
3232

33-
public func removeObject(forKey key: String, completion: @escaping (Result<()>) -> Void) {
33+
public func removeObject(forKey key: Key, completion: @escaping (Result<()>) -> Void) {
3434
serialQueue.async { [weak self] in
3535
guard let `self` = self else {
3636
completion(Result.error(StorageError.deallocated))
@@ -47,8 +47,8 @@ extension AsyncStorage {
4747
}
4848

4949
public func setObject(
50-
_ object: T,
51-
forKey key: String,
50+
_ object: Value,
51+
forKey key: Key,
5252
expiry: Expiry? = nil,
5353
completion: @escaping (Result<()>) -> Void) {
5454
serialQueue.async { [weak self] in
@@ -98,18 +98,18 @@ extension AsyncStorage {
9898
}
9999
}
100100

101-
public func object(forKey key: String, completion: @escaping (Result<T>) -> Void) {
102-
entry(forKey: key, completion: { (result: Result<Entry<T>>) in
101+
public func object(forKey key: Key, completion: @escaping (Result<Value>) -> Void) {
102+
entry(forKey: key, completion: { (result: Result<Entry<Value>>) in
103103
completion(result.map({ entry in
104104
return entry.object
105105
}))
106106
})
107107
}
108108

109109
public func existsObject(
110-
forKey key: String,
110+
forKey key: Key,
111111
completion: @escaping (Result<Bool>) -> Void) {
112-
object(forKey: key, completion: { (result: Result<T>) in
112+
object(forKey: key, completion: { (result: Result<Value>) in
113113
completion(result.map({ _ in
114114
return true
115115
}))
@@ -118,8 +118,8 @@ extension AsyncStorage {
118118
}
119119

120120
public extension AsyncStorage {
121-
func transform<U>(transformer: Transformer<U>) -> AsyncStorage<U> {
122-
let storage = AsyncStorage<U>(
121+
func transform<U>(transformer: Transformer<U>) -> AsyncStorage<Key, U> {
122+
let storage = AsyncStorage<Key, U>(
123123
storage: innerStorage.transform(transformer: transformer),
124124
serialQueue: serialQueue
125125
)

Source/Shared/Storage/DiskStorage.swift

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// Save objects to file on disk
4-
final public class DiskStorage<T> {
4+
final public class DiskStorage<Key: Hashable, Value> {
55
enum Error: Swift.Error {
66
case fileEnumeratorFailed
77
}
@@ -15,10 +15,11 @@ final public class DiskStorage<T> {
1515
/// The closure to be called when single file has been removed
1616
var onRemove: ((String) -> Void)?
1717

18-
private let transformer: Transformer<T>
18+
private let transformer: Transformer<Value>
19+
private let hasher = Hasher.constantAccrossExecutions()
1920

2021
// MARK: - Initialization
21-
public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<T>) throws {
22+
public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<Value>) throws {
2223
let url: URL
2324
if let directory = config.directory {
2425
url = directory
@@ -48,7 +49,7 @@ final public class DiskStorage<T> {
4849
#endif
4950
}
5051

51-
public required init(config: DiskConfig, fileManager: FileManager = FileManager.default, path: String, transformer: Transformer<T>) {
52+
public required init(config: DiskConfig, fileManager: FileManager = FileManager.default, path: String, transformer: Transformer<Value>) {
5253
self.config = config
5354
self.fileManager = fileManager
5455
self.path = path
@@ -57,7 +58,7 @@ final public class DiskStorage<T> {
5758
}
5859

5960
extension DiskStorage: StorageAware {
60-
public func entry(forKey key: String) throws -> Entry<T> {
61+
public func entry(forKey key: Key) throws -> Entry<Value> {
6162
let filePath = makeFilePath(for: key)
6263
let data = try Data(contentsOf: URL(fileURLWithPath: filePath))
6364
let attributes = try fileManager.attributesOfItem(atPath: filePath)
@@ -74,15 +75,15 @@ extension DiskStorage: StorageAware {
7475
)
7576
}
7677

77-
public func setObject(_ object: T, forKey key: String, expiry: Expiry? = nil) throws {
78+
public func setObject(_ object: Value, forKey key: Key, expiry: Expiry? = nil) throws {
7879
let expiry = expiry ?? config.expiry
7980
let data = try transformer.toData(object)
8081
let filePath = makeFilePath(for: key)
8182
_ = fileManager.createFile(atPath: filePath, contents: data, attributes: nil)
8283
try fileManager.setAttributes([.modificationDate: expiry.date], ofItemAtPath: filePath)
8384
}
8485

85-
public func removeObject(forKey key: String) throws {
86+
public func removeObject(forKey key: Key) throws {
8687
let filePath = makeFilePath(for: key)
8788
try fileManager.removeItem(atPath: filePath)
8889
onRemove?(filePath)
@@ -160,24 +161,30 @@ extension DiskStorage {
160161
- Parameter key: Unique key to identify the object in the cache
161162
- Returns: A md5 string
162163
*/
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)"
164+
func makeFileName(for key: Key) -> String {
165+
if let key = key as? String {
166+
let fileExtension = URL(fileURLWithPath: key).pathExtension
167+
let fileName = MD5(key)
168+
169+
switch fileExtension.isEmpty {
170+
case true:
171+
return fileName
172+
case false:
173+
return "\(fileName).\(fileExtension)"
174+
}
172175
}
176+
177+
var hasher = self.hasher
178+
key.hash(into: &hasher)
179+
return String(hasher.finalize())
173180
}
174181

175182
/**
176183
Builds file path from the key.
177184
- Parameter key: Unique key to identify the object in the cache
178185
- Returns: A string path based on key
179186
*/
180-
func makeFilePath(for key: String) -> String {
187+
func makeFilePath(for key: Key) -> String {
181188
return "\(path)/\(makeFileName(for: key))"
182189
}
183190

@@ -244,7 +251,7 @@ extension DiskStorage {
244251
Removes the object from the cache if it's expired.
245252
- Parameter key: Unique key to identify the object in the cache
246253
*/
247-
func removeObjectIfExpired(forKey key: String) throws {
254+
func removeObjectIfExpired(forKey key: Key) throws {
248255
let filePath = makeFilePath(for: key)
249256
let attributes = try fileManager.attributesOfItem(atPath: filePath)
250257
if let expiryDate = attributes[.modificationDate] as? Date, expiryDate.inThePast {
@@ -255,8 +262,8 @@ extension DiskStorage {
255262
}
256263

257264
public extension DiskStorage {
258-
func transform<U>(transformer: Transformer<U>) -> DiskStorage<U> {
259-
let storage = DiskStorage<U>(
265+
func transform<U>(transformer: Transformer<U>) -> DiskStorage<Key, U> {
266+
let storage = DiskStorage<Key, U>(
260267
config: config,
261268
fileManager: fileManager,
262269
path: path,

0 commit comments

Comments
 (0)