<NME> MockImageCache.swift
<BEF> // The MIT License (MIT)
//
// Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).

import Foundation
@testable import Nuke

class MockCache: Caching {
    var enabled = true
    var images = [URL: Image]()
    init() {}

    func image(for request: Request) -> Image? {
        return enabled ? images[request.urlRequest.url!] : nil
    }
    
    func setImage(_ image: Image, for request: Request) {
        if enabled {
            images[request.urlRequest.url!] = image
        }
    }
    
    func removeImage(for request: Request) {
        if enabled {
            images[request.urlRequest.url!] = nil
        }
    }
    
    func clear() {
        images.removeAll()
    }
                }
            }
        }
    }

    func removeAll() {
        images.removeAll()
    }
}

<MSG> Simplify Request equating model

<DFF> @@ -7,25 +7,29 @@ import Nuke
 
 class MockCache: Caching {
     var enabled = true
-    var images = [URL: Image]()
+    var images = [AnyHashable: Image]()
     init() {}
 
-    func image(for request: Request) -> Image? {
-        return enabled ? images[request.urlRequest.url!] : nil
+    func image(for key: AnyHashable) -> Image? {
+        return enabled ? images[key] : nil
     }
-    
-    func setImage(_ image: Image, for request: Request) {
+
+    func setImage(_ image: Image, for key: AnyHashable) {
         if enabled {
-            images[request.urlRequest.url!] = image
+            images[key] = image
         }
     }
-    
-    func removeImage(for request: Request) {
+
+    func removeImage(for key: AnyHashable) {
         if enabled {
-            images[request.urlRequest.url!] = nil
+            images[key] = nil
         }
     }
     
+    func removeImage(for request: Request) {
+        removeImage(for: Request.cacheKey(for: request))
+    }
+    
     func clear() {
         images.removeAll()
     }

{'commit': 'diff --git a/Sources/Cache.swift b/Sources/Cache.swift\nindex 2e3eaee1..5d0d0f46 100644\n--- a/Sources/Cache.swift\n+++ b/Sources/Cache.swift\n@@ -13,11 +13,23 @@ import Foundation\n ///\n /// The implementation is expected to be thread safe.\n public protocol Caching {\n+    /// Returns an image for the key.\n+    func image(for key: AnyHashable) -> Image?\n+\n+    /// Stores the image for the key.\n+    func setImage(_ image: Image, for key: AnyHashable)\n+}\n+\n+public extension Caching {\n     /// Returns an image for the request.\n-    func image(for request: Request) -> Image?\n+    public func image(for request: Request) -> Image? {\n+        return image(for: Request.cacheKey(for: request))\n+    }\n \n     /// Stores the image for the request.\n-    func setImage(_ image: Image, for request: Request)\n+    func setImage(_ image: Image, for request: Request) {\n+        setImage(image, for: Request.cacheKey(for: request))\n+    }\n }\n \n /// Auto purging memory cache that uses `NSCache` as its internal storage.\n@@ -33,12 +45,9 @@ open class Cache: Caching {\n     /// The internal memory cache.\n     public let cache: NSCache<AnyObject, AnyObject>\n \n-    private let equator: RequestEquating\n-\n     /// Initializes the receiver with a given memory cache.\n-    public init(cache: NSCache<AnyObject, AnyObject> = Cache.makeDefaultCache(), equator: RequestEquating = RequestCachingEquator()) {\n+    public init(cache: NSCache<AnyObject, AnyObject> = Cache.makeDefaultCache()) {\n         self.cache = cache\n-        self.equator = equator\n         #if os(iOS) || os(tvOS)\n             NotificationCenter.default.addObserver(self, selector: #selector(didReceiveMemoryWarning(_:)), name: .UIApplicationDidReceiveMemoryWarning, object: nil)\n         #endif\n@@ -58,23 +67,24 @@ open class Cache: Caching {\n     \n     // MARK: Managing Cached Images\n \n-    /// Returns an image for the request.\n-    open func image(for request: Request) -> Image? {\n-        return cache.object(forKey: makeKey(for: request)) as? Image\n+    /// Stores the image for the key.\n+    open func image(for key: AnyHashable) -> Image? {\n+        return cache.object(forKey: AnyHashableObject(key)) as? Image\n     }\n \n-    /// Stores the image for the request.\n-    open func setImage(_ image: Image, for request: Request) {\n-        cache.setObject(image, forKey: makeKey(for: request), cost: cost(for: image))\n+    /// Stores the image for the key.\n+    open func setImage(_ image: Image, for key: AnyHashable) {\n+        cache.setObject(image, forKey: AnyHashableObject(key), cost: cost(for: image))\n     }\n \n-    /// Removes an image for the request.\n-    open func removeImage(for request: Request) {\n-        cache.removeObject(forKey: makeKey(for: request))\n+    /// Removes an image for the key.\n+    open func removeImage(for key: AnyHashable) {\n+        cache.removeObject(forKey: AnyHashableObject(key))\n     }\n \n-    private func makeKey(for request: Request) -> AnyHashableObject {\n-        return AnyHashableObject(RequestKey(request, equator: equator))\n+    /// Removes an image for the request.\n+    open func removeImage(for request: Request) {\n+        removeImage(for: Request.cacheKey(for: request))\n     }\n \n     // MARK: Subclassing Hooks\ndiff --git a/Sources/Deduplicator.swift b/Sources/Deduplicator.swift\nindex 2f80c844..17ba04d4 100644\n--- a/Sources/Deduplicator.swift\n+++ b/Sources/Deduplicator.swift\n@@ -11,25 +11,21 @@ import Foundation\n /// The image will be loaded just once.\n public final class Deduplicator: Loading {\n     private let loader: Loading\n-    private let equator: RequestEquating\n-    private var tasks = [RequestKey: Task]()\n+    private var tasks = [AnyHashable: Task]()\n     private let queue = DispatchQueue(label: "\\(domain).Deduplicator")\n \n     /// Initializes the `Deduplicator` instance with the underlying\n     /// `loader` used for actual image loading, and the request `equator`.\n     /// - parameter loader: Underlying loader used for loading images.\n-    /// - parameter equator: Compares requests for equivalence.\n-    /// `RequestLoadingEquator()` be default.\n-    public init(with loader: Loading, equator: RequestEquating = RequestLoadingEquator()) {\n+    public init(with loader: Loading) {\n         self.loader = loader\n-        self.equator = equator\n     }\n \n     /// Returns an existing pending promise if there is one. Starts a new load\n     /// request otherwise.\n     public func loadImage(with request: Request, token: CancellationToken? = nil) -> Promise<Image> {\n         return queue.sync {\n-            let key = RequestKey(request, equator: equator)\n+            let key = Request.loadKey(for: request)\n             var task: Task! = tasks[key] // Find existing promise\n             if task == nil {\n                 let cts = CancellationTokenSource()\ndiff --git a/Sources/Preheater.swift b/Sources/Preheater.swift\nindex 57d86004..90423e12 100644\n--- a/Sources/Preheater.swift\n+++ b/Sources/Preheater.swift\n@@ -15,20 +15,16 @@ import Foundation\n /// When preheating is no longer necessary call `stopPreheating(with:)` method.\n public class Preheater {\n     private let loader: Loading\n-    private let equator: RequestEquating\n     private let scheduler: AsyncScheduler\n     private let queue = DispatchQueue(label: "\\(domain).Preheater")\n     private var tasks = [Task]()\n         \n     /// Initializes the `Preheater` instance.\n     /// - parameter loader: `Loader.shared` by default.\n-    /// - parameter equator: Compares requests for equivalence.\n-    /// `RequestLoadingEquator()` be default.\n     /// - parameter scheduler: Throttles preheating requests. `QueueScheduler`\n     /// with `maxConcurrentOperationCount` 2 by default.\n-    public init(loader: Loading = Loader.shared, equator: RequestEquating = RequestLoadingEquator(), scheduler: AsyncScheduler = QueueScheduler(maxConcurrentOperationCount: 2)) {\n+    public init(loader: Loading = Loader.shared, scheduler: AsyncScheduler = QueueScheduler(maxConcurrentOperationCount: 2)) {\n         self.loader = loader\n-        self.equator = equator\n         self.scheduler = scheduler\n     }\n \n@@ -75,7 +71,8 @@ public class Preheater {\n     }\n     \n     private func indexOfTask(with request: Request) -> Int? {\n-        return tasks.index { equator.isEqual($0.request, to: request) }\n+        let key = Request.loadKey(for: request)\n+        return tasks.index { key == Request.loadKey(for: $0.request) }\n     }\n \n     /// Stops all preheating tasks.\ndiff --git a/Sources/Request.swift b/Sources/Request.swift\nindex 3b32459c..ecdb4b40 100644\n--- a/Sources/Request.swift\n+++ b/Sources/Request.swift\n@@ -35,6 +35,16 @@ public struct Request {\n     /// `MemoryCacheOptions()` by default.\n     public var memoryCacheOptions = MemoryCacheOptions()\n \n+    /// Returns key which compares requests with regards to loading images.\n+    ///\n+    /// If `nil` default key is used. `nil` by default.\n+    public var loadKey: AnyHashable?\n+\n+    /// Returns key which compares requests with regards to cachings images.\n+    ///\n+    /// If `nil` default key is used. `nil` by default.\n+    public var cacheKey: AnyHashable?\n+\n     /// Allows you to pass custom info alongside the request.\n     public var userInfo: Any?\n }\n@@ -46,68 +56,56 @@ public extension Request {\n         request.processors.append(AnyProcessor(processor))\n         return request\n     }\n-\n+    \n     /// Wraps processors into ProcessorComposition.\n     internal var processor: ProcessorComposition? {\n         return processors.isEmpty ? nil : ProcessorComposition(processors: processors)\n     }\n }\n \n-// MARK: - RequestEquating\n-\n-/// Tests requests for equivalence in various contexts (caching, loading, etc).\n-public protocol RequestEquating {\n-    func isEqual(_ a: Request, to b: Request) -> Bool\n-}\n-\n-/// Considers two requests equivalent it they have the same `URLRequests` and\n-/// the same processors. `URLRequests` are compared by their `URL`, `cachePolicy`,\n-/// and `allowsCellularAccess` properties. To change this behaviour just create\n-/// a new `RequestEquating` type.\n-public struct RequestLoadingEquator: RequestEquating {\n-    public init() {}\n-    \n-    public func isEqual(_ a: Request, to b: Request) -> Bool {\n-        return isEqual(a.urlRequest, to: b.urlRequest) && a.processor == b.processor\n+public extension Request {\n+    /// Returns key which compares requests with regards to cachings images.\n+    /// Returns `cacheKey` if not nil. Returns default key otherwise.\n+    ///\n+    /// The default key considers two requests equivalent it they have the same\n+    /// `URLRequests` and the same processors. `URLRequests` are compared\n+    /// just by their `URLs`.\n+    public static func cacheKey(for request: Request) -> AnyHashable {\n+        return request.cacheKey ?? AnyHashable(RequestKey(request: request) {\n+            $0.urlRequest.url == $0.urlRequest.url && $1.processor == $1.processor\n+        })\n     }\n     \n-    private func isEqual(_ a: URLRequest, to b: URLRequest) -> Bool {\n-        return a.url == b.url &&\n-            a.cachePolicy == b.cachePolicy &&\n-            a.allowsCellularAccess == b.allowsCellularAccess\n+    /// Returns key which compares requests with regards to loading images.\n+    /// Returns `loadKey` if not nil. Returns default key otherwise.\n+    ///\n+    /// The default key considers two requests equivalent it they have the same\n+    /// `URLRequests` and the same processors. `URLRequests` are compared by\n+    /// their `URL`, `cachePolicy`, and `allowsCellularAccess` properties.\n+    public static func loadKey(for request: Request) -> AnyHashable {\n+        func isEqual(_ a: URLRequest, to b: URLRequest) -> Bool {\n+            return a.url == b.url &&\n+                a.cachePolicy == b.cachePolicy &&\n+                a.allowsCellularAccess == b.allowsCellularAccess\n+        }\n+        return request.loadKey ?? AnyHashable(RequestKey(request: request) {\n+            isEqual($0.urlRequest, to: $1.urlRequest) && $0.processor == $1.processor\n+        })\n     }\n }\n \n-/// Considers two requests equivalent it they have the same `URLRequests` and\n-/// the same processors. `URLRequests` are compared just by their `URLs`.\n-/// To change this behaviour just create a new `RequestEquating` type.\n-public struct RequestCachingEquator: RequestEquating {\n-    public init() {}\n+/// Compares two requests for equivalence using an `equator` closure.\n+private struct RequestKey: Hashable {\n+    let request: Request\n+    let equator: (Request, Request) -> Bool\n     \n-    public func isEqual(_ a: Request, to b: Request) -> Bool {\n-        return a.urlRequest.url == b.urlRequest.url && a.processor == b.processor\n-    }\n-}\n-\n-// MARK: - RequestKey\n-\n-/// Makes it possible to use Request as a key.\n-internal struct RequestKey: Hashable {\n-    private let request: Request\n-    private let equator: RequestEquating\n-\n-    init(_ request: Request, equator: RequestEquating) {\n-        self.request = request\n-        self.equator = equator\n-    }\n-\n     /// Returns hash from the request\'s URL.\n     var hashValue: Int {\n         return request.urlRequest.url?.hashValue ?? 0\n     }\n-\n+    \n     /// Compares two keys for equivalence.\n     static func ==(lhs: RequestKey, rhs: RequestKey) -> Bool {\n-        return lhs.equator.isEqual(lhs.request, to: rhs.request)\n+        return lhs.equator(lhs.request, rhs.request)\n     }\n }\ndiff --git a/Tests/DeduplicatorTests.swift b/Tests/DeduplicatorTests.swift\nindex 3e9d0f68..4117ecdf 100644\n--- a/Tests/DeduplicatorTests.swift\n+++ b/Tests/DeduplicatorTests.swift\n@@ -21,7 +21,7 @@ class DeduplicatorTests: XCTestCase {\n         \n         let request1 = Request(url: defaultURL)\n         let request2 = Request(url: defaultURL)\n-        XCTAssertTrue(RequestLoadingEquator().isEqual(request1, to: request2))\n+        XCTAssertTrue(Request.loadKey(for: request1) == Request.loadKey(for: request2))\n \n         expect { fulfill in\n             _ = deduplicator.loadImage(with: request1).then { _ in\n@@ -45,7 +45,7 @@ class DeduplicatorTests: XCTestCase {\n     func testThatNonEquivalentRequestsAreNotDeduplicated() {\n         let request1 = Request(urlRequest: URLRequest(url: defaultURL, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 0))\n         let request2 = Request(urlRequest: URLRequest(url: defaultURL, cachePolicy: .returnCacheDataDontLoad, timeoutInterval: 0))\n-        XCTAssertFalse(RequestLoadingEquator().isEqual(request1, to: request2))\n+        XCTAssertFalse(Request.loadKey(for: request1) == Request.loadKey(for: request2))\n                 \n         expect { fulfill in\n             _ = deduplicator.loadImage(with: request1).then { _ in\ndiff --git a/Tests/MockImageCache.swift b/Tests/MockImageCache.swift\nindex 7c2af3d9..d11fbacb 100644\n--- a/Tests/MockImageCache.swift\n+++ b/Tests/MockImageCache.swift\n@@ -7,25 +7,29 @@ import Nuke\n \n class MockCache: Caching {\n     var enabled = true\n-    var images = [URL: Image]()\n+    var images = [AnyHashable: Image]()\n     init() {}\n \n-    func image(for request: Request) -> Image? {\n-        return enabled ? images[request.urlRequest.url!] : nil\n+    func image(for key: AnyHashable) -> Image? {\n+        return enabled ? images[key] : nil\n     }\n-    \n-    func setImage(_ image: Image, for request: Request) {\n+\n+    func setImage(_ image: Image, for key: AnyHashable) {\n         if enabled {\n-            images[request.urlRequest.url!] = image\n+            images[key] = image\n         }\n     }\n-    \n-    func removeImage(for request: Request) {\n+\n+    func removeImage(for key: AnyHashable) {\n         if enabled {\n-            images[request.urlRequest.url!] = nil\n+            images[key] = nil\n         }\n     }\n     \n+    func removeImage(for request: Request) {\n+        removeImage(for: Request.cacheKey(for: request))\n+    }\n+    \n     func clear() {\n         images.removeAll()\n     }\n', 'commit_subject': 'Simplify Request equating model', 'repo_name': 'kean/Nuke', 'committer': {'name': 'kean', 'email': '7d79009c44fb8cc76c73cdb194cbb1e3b38c7a62@gmail.com', 'time_sec': '1471980545', 'tz_offset': '180', 'date': {'seconds': '1471980545'}}, 'commit_tree': '380a46d32df91adba890ce337a59671ba3179230', 'commit_message': 'Simplify Request equating model\n', 'language_name': 'Swift', 'license': 'mit', 'hunks': '@@ -7,25 +7,29 @@ import Nuke\n \n class MockCache: Caching {\n     var enabled = true\n-    var images = [URL: Image]()\n+    var images = [AnyHashable: Image]()\n     init() {}\n \n-    func image(for request: Request) -> Image? {\n-        return enabled ? images[request.urlRequest.url!] : nil\n+    func image(for key: AnyHashable) -> Image? {\n+        return enabled ? images[key] : nil\n     }\n-    \n-    func setImage(_ image: Image, for request: Request) {\n+\n+    func setImage(_ image: Image, for key: AnyHashable) {\n         if enabled {\n-            images[request.urlRequest.url!] = image\n+            images[key] = image\n         }\n     }\n-    \n-    func removeImage(for request: Request) {\n+\n+    func removeImage(for key: AnyHashable) {\n         if enabled {\n-            images[request.urlRequest.url!] = nil\n+            images[key] = nil\n         }\n     }\n     \n+    func removeImage(for request: Request) {\n+        removeImage(for: Request.cacheKey(for: request))\n+    }\n+    \n     func clear() {\n         images.removeAll()\n     }\n', 'addition_count': 13, 'deletion_count': 9, 'src_file': 'a/Tests/MockImageCache.swift', 'tgt_file': 'b/Tests/MockImageCache.swift', 'file_extension': '.swift', 'before_file': '// The MIT License (MIT)\n//\n// Copyright (c) 2015-2022 Alexander Grebenyuk (github.com/kean).\n\nimport Foundation\n@testable import Nuke\n\nclass MockCache: Caching {\n    var enabled = true\n    var images = [URL: Image]()\n    init() {}\n\n    func image(for request: Request) -> Image? {\n        return enabled ? images[request.urlRequest.url!] : nil\n    }\n    \n    func setImage(_ image: Image, for request: Request) {\n        if enabled {\n            images[request.urlRequest.url!] = image\n        }\n    }\n    \n    func removeImage(for request: Request) {\n        if enabled {\n            images[request.urlRequest.url!] = nil\n        }\n    }\n    \n    func clear() {\n        images.removeAll()\n    }\n                }\n            }\n        }\n    }\n\n    func removeAll() {\n        images.removeAll()\n    }\n}\n', 'message': 'Simplify Request equating model\n', 'author_name': 'kean', 'lang': 'swift'}