Skip to content

Commit

Permalink
Strict Concurrency Compatibility (#9)
Browse files Browse the repository at this point in the history
* Declare (conditional) Sendable conformance

* DiskCache -> 2.1.0

* DiskCache -> 2.2.0

* Make MockCache Sendable

* More descriptive error message

* Fix test failure

* Fix Swift 6 warnings

* Fix tests

* Fix CodableCaching tests
  • Loading branch information
mgacy authored Jul 30, 2024
1 parent 0baace5 commit 186fe87
Show file tree
Hide file tree
Showing 11 changed files with 163 additions and 58 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/Mobelux/DiskCache.git",
"state": {
"branch": null,
"revision": "dc9d51b67abefb41669cee38534cf86ab92ea610",
"version": "2.0.0"
"revision": "1624e932c3cde0f221fc8c340fda263f72c87f69",
"version": "2.2.0"
}
},
{
Expand Down
6 changes: 1 addition & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,15 @@ let package = Package(
.macOS(.v10_15)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "CodableCache",
targets: ["CodableCache"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/Mobelux/DiskCache.git", from: "2.0.0"),
.package(url: "https://github.com/Mobelux/DiskCache.git", from: "2.2.0"),
.package(url: "https://github.com/apple/swift-crypto.git", from: "1.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "CodableCache",
dependencies: [
Expand Down
41 changes: 20 additions & 21 deletions Sources/CodableCache/CodableCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,37 @@ import DiskCache
import Foundation

/// A cache for `Codable` values.
public final class CodableCache {
internal static var encoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601

return encoder
}()

internal static var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

return decoder
}()

internal static var makeDate: () -> Date = { Date() }

public final class CodableCache: Sendable {
private let cache: Cache
private let decoder: JSONDecoder
private let encoder: JSONEncoder
private let makeDate: @Sendable () -> Date

/// Initilizes an instance of `CodableCache`.
/// - Parameter cache: A type conforming to the `Cache` protocol.
public init(_ cache: Cache) {
public convenience init(_ cache: Cache) {
self.init(cache: cache)
}

internal init(
cache: any Cache,
decoder: JSONDecoder = .iso8601,
encoder: JSONEncoder = .iso8601,
makeDate: @escaping @Sendable () -> Date = { Date() }
) {
self.cache = cache
self.decoder = decoder
self.encoder = encoder
self.makeDate = makeDate
}

/// Asynchronously caches the given object.
/// - Parameter object: The object which should be cached. It must conform to `Codable`.
/// - Parameter key: A unique key used to identify the cached object.
/// - Parameter ttl: Defines the amount of time the cached object is valid.
public func cache<T: Codable>(object: T, key: Keyable, ttl: TTL = TTL.default) async throws {
let wrapper = CacheWrapper(ttl: ttl, created: Self.makeDate(), object: object)
try await cache.cache(Self.encoder.encode(wrapper), key: key.rawValue)
let wrapper = CacheWrapper(ttl: ttl, created: makeDate(), object: object)
try await cache.cache(encoder.encode(wrapper), key: key.rawValue)
}

/// Deletes the cached object associated with the given key.
Expand All @@ -59,7 +58,7 @@ public final class CodableCache {
public func object<T: Codable>(key: Keyable) async -> T? {
do {
let data = try await self.cache.data(key.rawValue)
let wrapper = try Self.decoder.decode(CacheWrapper<T>.self, from: data)
let wrapper = try decoder.decode(CacheWrapper<T>.self, from: data)
if wrapper.isObjectStale {
try await delete(objectWith: key)
return nil
Expand Down
44 changes: 37 additions & 7 deletions Sources/CodableCache/CodableCaching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import Foundation
public final class CodableCaching<Value: Codable> {
private lazy var codableCache: CodableCache = {
do {
return try CodableCache(cache())
return try makeCodableCache()
} catch {
fatalError("Creating cache instance failed with error:\n\(error)")
}
}()

private let cache: () throws -> Cache
private let makeCodableCache: @Sendable () throws -> CodableCache
private let key: Keyable
private let ttl: TTL

Expand Down Expand Up @@ -64,13 +64,43 @@ public final class CodableCaching<Value: Codable> {
/// - key: A unique key used to identify the cached object.
/// - cache: A function defining a type conforming to `Cache` to use as backing storage.
/// - ttl: Defines the amount of time the cached object is valid.
public init(wrappedValue: Value? = nil,
key: Keyable,
cache: @escaping () throws -> Cache = { try DiskCache(storageType: .temporary(.custom("codable-cache"))) },
ttl: TTL = .default) {
public convenience init(
wrappedValue: Value? = nil,
key: Keyable,
cache: @escaping @Sendable () throws -> any Cache = { try DiskCache(storageType: .temporary(.custom("codable-cache"))) },
ttl: TTL = .default
) {
self.init(
wrappedValue: wrappedValue,
key: key,
makeCodableCache: { try CodableCache(cache()) },
ttl: ttl)
}

internal convenience init(
wrappedValue: Value? = nil,
key: any Keyable,
cache: @escaping @Sendable () throws -> any Cache,
encoder: JSONEncoder,
makeDate: @escaping @Sendable () -> Date,
ttl: TTL = .default
) {
self.init(
wrappedValue: wrappedValue,
key: key,
makeCodableCache: { try CodableCache(cache: cache(), encoder: encoder, makeDate: makeDate) },
ttl: ttl)
}

internal init(
wrappedValue: Value? = nil,
key: any Keyable,
makeCodableCache: @escaping @Sendable () throws -> CodableCache,
ttl: TTL = .default
) {
self.wrappedValue = wrappedValue
self.key = key
self.cache = cache
self.makeCodableCache = makeCodableCache
self.ttl = ttl
}
}
17 changes: 17 additions & 0 deletions Sources/CodableCache/Extensions/JSONDecoder+Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// JSONDecoder+Utils.swift
//
//
// Created by Mathew Gacy on 7/8/24.
//

import Foundation

extension JSONDecoder {
static let iso8601: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

return decoder
}()
}
25 changes: 25 additions & 0 deletions Sources/CodableCache/Extensions/JSONEncoder+Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// JSONEncoder+Utils.swift
//
//
// Created by Mathew Gacy on 7/8/24.
//

import Foundation

extension JSONEncoder {
static let iso8601: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601

return encoder
}()

static let sorted: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = .sortedKeys

return encoder
}()
}
2 changes: 2 additions & 0 deletions Sources/CodableCache/Models/CacheWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ extension CacheWrapper {
return abs(created.timeIntervalSinceNow) >= TimeInterval(ttl.value)
}
}

extension CacheWrapper: Sendable where T: Sendable {}
2 changes: 1 addition & 1 deletion Sources/CodableCache/Models/TTL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import Foundation

/// Specifies the time-to-live for a cached object.
public enum TTL: Codable {
public enum TTL: Codable, Sendable {
private enum CodingKeys: String, CodingKey {
case second, minute, hour, day, forever
}
Expand Down
21 changes: 11 additions & 10 deletions Tests/CodableCacheTests/CodableCacheTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@ final class CodableCacheTests: XCTestCase {
}

static let test = Test(value: "test-value")
let encoder = JSONEncoder.sorted
var date = Date(timeIntervalSince1970: 0)
var wrapper: CacheWrapper<Test> {
CodableCacheTests.wrapper(for: CodableCacheTests.test, date: CodableCache.makeDate())
CodableCacheTests.wrapper(for: CodableCacheTests.test, date: date)
}

override func setUp() {
let date = Date()
CodableCache.makeDate = { date }
date = Date()
}

func testCache() async throws {
let data = try CodableCache.encoder.encode(wrapper)
let data = try encoder.encode(wrapper)

let mockCache = MockCache(instruction: .data(data))
let codableCache = CodableCache(mockCache)
let codableCache = CodableCache(cache: mockCache, encoder: encoder, makeDate: { self.date })

do {
try await codableCache.cache(object: Self.test, key: "test", ttl: .default)
Expand All @@ -53,23 +54,23 @@ final class CodableCacheTests: XCTestCase {
}

func testData() async throws {
let testData = try CodableCache.encoder.encode(wrapper)
let testData = try encoder.encode(wrapper)

let mockCache = MockCache(instruction: .data(testData))
let codableCache = CodableCache(mockCache)
let codableCache = CodableCache(cache: mockCache, encoder: encoder, makeDate: { self.date })

let data: Test? = await codableCache.object(key: "test")
XCTAssertEqual(data, Self.test)
XCTAssertEqual(mockCache.callable, .data)
}

func testStaleData() async throws {
CodableCache.makeDate = { Date(timeIntervalSinceNow: -Double(TTL.day(2).value)) }
let testData = try CodableCache.encoder.encode(wrapper)
date = Date(timeIntervalSinceNow: -Double(TTL.day(2).value))
let testData = try encoder.encode(wrapper)
let testError = "throwing-data"

let mockCache = MockCache(instruction: .dataThrow(testData, testError))
let codableCache = CodableCache(mockCache)
let codableCache = CodableCache(cache: mockCache, encoder: encoder, makeDate: { self.date })

let data: Test? = await codableCache.object(key: "test")
XCTAssertEqual(data, nil)
Expand Down
8 changes: 6 additions & 2 deletions Tests/CodableCacheTests/CodableCachingTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,19 @@ import XCTest
@testable import CodableCache

class CodableCachingTest: XCTestCase {
static let date = Date()

@CodableCaching(
key: "test",
cache: { MockCache(
instruction: .data(
try! CodableCache.encoder.encode(CacheWrapper(
try! JSONEncoder.sorted.encode(CacheWrapper(
ttl: .default,
created: CodableCache.makeDate(),
created: date,
object: "test-value"))
)) },
encoder: .sorted,
makeDate: { CodableCachingTest.date },
ttl: .default)
var testValue: String?

Expand Down
Loading

0 comments on commit 186fe87

Please sign in to comment.