Skip to content

Commit 1b627dc

Browse files
Add support for custom decoding/encoding to fileStorage (#3225)
* Add support for custom decoding/encoding to `fileStorage` * Update FileStorageKey.swift --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent 54eb417 commit 1b627dc

File tree

2 files changed

+72
-9
lines changed

2 files changed

+72
-9
lines changed

Sources/ComposableArchitecture/SharedState/PersistenceKey/FileStorageKey.swift

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,52 @@ import Dependencies
33
import Foundation
44

55
extension PersistenceReaderKey {
6-
/// Creates a persistence key that can read and write to a `Codable` value to the file system.
6+
/// Creates a persistence key that can read and write to a `Codable` value in the file system.
77
///
8-
/// - Parameter url: The file URL from which to read and write the value.
8+
/// - Parameters:
9+
/// - url: The file URL from which to read and write the value.
10+
/// - decoder: The JSONDecoder to use for decoding the value.
11+
/// - encoder: The JSONEncoder to use for encoding the value.
912
/// - Returns: A file persistence key.
10-
public static func fileStorage<Value: Codable>(_ url: URL) -> Self
13+
public static func fileStorage<Value: Codable>(
14+
_ url: URL,
15+
decoder: JSONDecoder = JSONDecoder(),
16+
encoder: JSONEncoder = JSONEncoder()
17+
) -> Self
1118
where Self == FileStorageKey<Value> {
12-
FileStorageKey(url: url)
19+
FileStorageKey(
20+
url: url,
21+
decode: { try decoder.decode(Value.self, from: $0) },
22+
encode: { try encoder.encode($0) }
23+
)
24+
}
25+
26+
/// Creates a persistence key that can read and write to a value in the file system.
27+
///
28+
/// - Parameters:
29+
/// - url: The file URL from which to read and write the value.
30+
/// - decode: The closure to use for decoding the value.
31+
/// - encode: The closure to use for encoding the value.
32+
/// - Returns: A file persistence key.
33+
public static func fileStorage<Value>(
34+
_ url: URL,
35+
decode: @escaping @Sendable (Data) throws -> Value,
36+
encode: @escaping @Sendable (Value) throws -> Data
37+
) -> Self
38+
where Self == FileStorageKey<Value> {
39+
FileStorageKey(url: url, decode: decode, encode: encode)
1340
}
1441
}
1542

1643
/// A type defining a file persistence strategy
1744
///
1845
/// Use ``PersistenceReaderKey/fileStorage(_:)`` to create values of this type.
19-
public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Sendable {
46+
public final class FileStorageKey<Value: Sendable>: PersistenceKey, Sendable {
2047
private let storage: FileStorage
2148
private let isSetting = LockIsolated(false)
2249
private let url: URL
50+
private let decode: @Sendable (Data) throws -> Value
51+
private let encode: @Sendable (Value) throws -> Data
2352
fileprivate let state = LockIsolated(State())
2453
// private let value = LockIsolated<Value?>(nil)
2554
// private let workItem = LockIsolated<DispatchWorkItem?>(nil)
@@ -33,15 +62,21 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
3362
FileStorageKeyID(url: self.url, storage: self.storage)
3463
}
3564

36-
fileprivate init(url: URL) {
65+
fileprivate init(
66+
url: URL,
67+
decode: @escaping @Sendable (Data) throws -> Value,
68+
encode: @escaping @Sendable (Value) throws -> Data
69+
) {
3770
@Dependency(\.defaultFileStorage) var storage
3871
self.storage = storage
3972
self.url = url
73+
self.decode = decode
74+
self.encode = encode
4075
}
4176

4277
public func load(initialValue: Value?) -> Value? {
4378
do {
44-
return try JSONDecoder().decode(Value.self, from: self.storage.load(self.url))
79+
return try decode(self.storage.load(self.url))
4580
} catch {
4681
return initialValue
4782
}
@@ -51,7 +86,7 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
5186
self.state.withValue { state in
5287
if state.workItem == nil {
5388
self.isSetting.setValue(true)
54-
try? self.storage.save(JSONEncoder().encode(value), self.url)
89+
try? self.storage.save(encode(value), self.url)
5590
let workItem = DispatchWorkItem { [weak self] in
5691
guard let self else { return }
5792
self.state.withValue { state in
@@ -62,7 +97,7 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
6297
guard let value = state.value
6398
else { return }
6499
self.isSetting.setValue(true)
65-
try? self.storage.save(JSONEncoder().encode(value), self.url)
100+
try? self.storage.save(self.encode(value), self.url)
66101
}
67102
}
68103
state.workItem = workItem

Tests/ComposableArchitectureTests/FileStorageTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,21 @@ final class FileStorageTests: XCTestCase {
1616
}
1717
}
1818

19+
func testBasics_CustomDecodeEncodeClosures() {
20+
let fileSystem = LockIsolated<[URL: Data]>([:])
21+
withDependencies {
22+
$0.defaultFileStorage = .inMemory(fileSystem: fileSystem, scheduler: .immediate)
23+
} operation: {
24+
@Shared(.utf8String) var string = ""
25+
XCTAssertNoDifference(fileSystem.value, [.utf8StringURL: Data()])
26+
string = "hello"
27+
XCTAssertNoDifference(
28+
fileSystem.value[.utf8StringURL].map { String(decoding: $0, as: UTF8.self) },
29+
"hello"
30+
)
31+
}
32+
}
33+
1934
func testThrottle() throws {
2035
let fileSystem = LockIsolated<[URL: Data]>([:])
2136
let testScheduler = DispatchQueue.test
@@ -464,13 +479,26 @@ final class FileStorageTests: XCTestCase {
464479
}
465480
}
466481

482+
extension PersistenceReaderKey
483+
where Self == FileStorageKey<String> {
484+
fileprivate static var utf8String: Self {
485+
.fileStorage(
486+
.utf8StringURL,
487+
decode: { data in String(decoding: data, as: UTF8.self) },
488+
encode: { string in Data(string.utf8) }
489+
)
490+
}
491+
}
492+
467493
extension URL {
468494
fileprivate static let fileURL = Self(fileURLWithPath: NSTemporaryDirectory())
469495
.appendingPathComponent("file.json")
470496
fileprivate static let userURL = Self(fileURLWithPath: NSTemporaryDirectory())
471497
.appendingPathComponent("user.json")
472498
fileprivate static let anotherFileURL = Self(fileURLWithPath: NSTemporaryDirectory())
473499
.appendingPathComponent("another-file.json")
500+
fileprivate static let utf8StringURL = Self(fileURLWithPath: NSTemporaryDirectory())
501+
.appendingPathComponent("utf8-string.json")
474502
}
475503

476504
private struct User: Codable, Equatable, Identifiable {

0 commit comments

Comments
 (0)