diff --git a/Sources/DictionaryCachableEncoder.swift b/Sources/DictionaryCachableEncoder.swift new file mode 100644 index 0000000..4b501ac --- /dev/null +++ b/Sources/DictionaryCachableEncoder.swift @@ -0,0 +1,45 @@ +import Foundation + +/* + An encoder that, if the value to encode is hashable, caches the encoded result. + This is useful to speed up applications that have to encode the same hashable value numerous times + The cache key is calculated from the combination of: + 1- The hashable value to enconde + 2- The userInfo dictionary (note that you have to provide a closure to convert it to a hashable object) + */ +open class DictionaryCachableEncoder: DictionaryEncoder { + open var userInfoHasher: ([CodingUserInfoKey: Any]) -> AnyHashable = { _ in AnyHashable(0) } + open var cache: CacheProtocol = DefaultCache.shared + + override func box(_ value: T) throws -> Any { + if let hashableValue = value as? AnyHashable { + let userInfoHash = userInfoHasher(userInfo) + let cacheKey = AnyHashable([hashableValue, userInfoHash]) + if let cached = cache.storage[cacheKey] { + return cached + } else { + let container = try super.box(value) + cache.storage[cacheKey] = container + return container + } + } else { + return try super.box(value) + } + } +} + +// MARK: cache related + +public protocol CacheProtocol: class { + var storage: [AnyHashable: Any] { get set } +} + +extension DictionaryCachableEncoder { + open class DefaultCache: CacheProtocol { + public static let shared = DefaultCache() + + open var storage: [AnyHashable: Any] = [:] + + public init() { } + } +} diff --git a/Sources/DictionaryEncoder.swift b/Sources/DictionaryEncoder.swift index dfea992..1438778 100644 --- a/Sources/DictionaryEncoder.swift +++ b/Sources/DictionaryEncoder.swift @@ -11,7 +11,7 @@ import Foundation open class DictionaryEncoder: Encoder { open var codingPath: [CodingKey] = [] open var userInfo: [CodingUserInfoKey: Any] = [:] - private var storage = Storage() + private(set) var storage = Storage() public init() {} @@ -27,7 +27,7 @@ open class DictionaryEncoder: Encoder { return SingleValueContainer(encoder: self, codingPath: codingPath) } - private func box(_ value: T) throws -> Any { + func box(_ value: T) throws -> Any { try value.encode(to: self) return storage.popContainer() } diff --git a/Tests/DictionaryCachableEncoderTests.swift b/Tests/DictionaryCachableEncoderTests.swift new file mode 100644 index 0000000..d93d5e8 --- /dev/null +++ b/Tests/DictionaryCachableEncoderTests.swift @@ -0,0 +1,74 @@ +import Foundation +import XCTest +import MoreCodable + +class DictionaryCachableEncoderTests: XCTestCase { + struct HashableUser: Encodable, Hashable { + let name: String + let age: Int + } + + let hashableUser = HashableUser(name: "Tatsuya Tanaka", age: 24) + + let cache = DictionaryCachableEncoder.DefaultCache() + + func buildEncoder() -> DictionaryCachableEncoder { + let encoder = DictionaryCachableEncoder() + encoder.cache = cache + return encoder + } + + func testSimpleModel() throws { + // First + do { + XCTAssertEqual(cache.storage.count, 0) + + let encoder = buildEncoder() + let dictionary = try encoder.encode(hashableUser) + XCTAssertEqual(hashableUser.name, dictionary["name"] as? String) + XCTAssertEqual(hashableUser.age, dictionary["age"] as? Int) + XCTAssertEqual(dictionary.keys.count, 2) + + XCTAssertEqual(cache.storage.count, 1) + } + + // Second + do { + XCTAssertEqual(cache.storage.count, 1) + + let encoder = buildEncoder() + let dictionary = try encoder.encode(hashableUser) + XCTAssertEqual(hashableUser.name, dictionary["name"] as? String) + XCTAssertEqual(hashableUser.age, dictionary["age"] as? Int) + XCTAssertEqual(dictionary.keys.count, 2) + + XCTAssertEqual(cache.storage.count, 1) + } + } + + func testSimpleModelWithCustomUserInfo() throws { + let encoder = buildEncoder() + encoder.userInfoHasher = { _ in 0 } + + do { + XCTAssertEqual(cache.storage.count, 0) + _ = try encoder.encode(hashableUser) + XCTAssertEqual(cache.storage.count, 1) + } + + do { + XCTAssertEqual(cache.storage.count, 1) + _ = try encoder.encode(hashableUser) + XCTAssertEqual(cache.storage.count, 1) + } + + encoder.userInfoHasher = { _ in 1 } + + do { + XCTAssertEqual(cache.storage.count, 1) + _ = try encoder.encode(hashableUser) + XCTAssertEqual(cache.storage.count, 2) + } + } + +}