Skip to content

Commit

Permalink
Add support for custom decoding/encoding to fileStorage (#3225)
Browse files Browse the repository at this point in the history
* Add support for custom decoding/encoding to `fileStorage`

* Update FileStorageKey.swift

---------

Co-authored-by: Stephen Celis <[email protected]>
  • Loading branch information
oskarek and stephencelis authored Jul 22, 2024
1 parent 54eb417 commit 1b627dc
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,52 @@ import Dependencies
import Foundation

extension PersistenceReaderKey {
/// Creates a persistence key that can read and write to a `Codable` value to the file system.
/// Creates a persistence key that can read and write to a `Codable` value in the file system.
///
/// - Parameter url: The file URL from which to read and write the value.
/// - Parameters:
/// - url: The file URL from which to read and write the value.
/// - decoder: The JSONDecoder to use for decoding the value.
/// - encoder: The JSONEncoder to use for encoding the value.
/// - Returns: A file persistence key.
public static func fileStorage<Value: Codable>(_ url: URL) -> Self
public static func fileStorage<Value: Codable>(
_ url: URL,
decoder: JSONDecoder = JSONDecoder(),
encoder: JSONEncoder = JSONEncoder()
) -> Self
where Self == FileStorageKey<Value> {
FileStorageKey(url: url)
FileStorageKey(
url: url,
decode: { try decoder.decode(Value.self, from: $0) },
encode: { try encoder.encode($0) }
)
}

/// Creates a persistence key that can read and write to a value in the file system.
///
/// - Parameters:
/// - url: The file URL from which to read and write the value.
/// - decode: The closure to use for decoding the value.
/// - encode: The closure to use for encoding the value.
/// - Returns: A file persistence key.
public static func fileStorage<Value>(
_ url: URL,
decode: @escaping @Sendable (Data) throws -> Value,
encode: @escaping @Sendable (Value) throws -> Data
) -> Self
where Self == FileStorageKey<Value> {
FileStorageKey(url: url, decode: decode, encode: encode)
}
}

/// A type defining a file persistence strategy
///
/// Use ``PersistenceReaderKey/fileStorage(_:)`` to create values of this type.
public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Sendable {
public final class FileStorageKey<Value: Sendable>: PersistenceKey, Sendable {
private let storage: FileStorage
private let isSetting = LockIsolated(false)
private let url: URL
private let decode: @Sendable (Data) throws -> Value
private let encode: @Sendable (Value) throws -> Data
fileprivate let state = LockIsolated(State())
// private let value = LockIsolated<Value?>(nil)
// private let workItem = LockIsolated<DispatchWorkItem?>(nil)
Expand All @@ -33,15 +62,21 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
FileStorageKeyID(url: self.url, storage: self.storage)
}

fileprivate init(url: URL) {
fileprivate init(
url: URL,
decode: @escaping @Sendable (Data) throws -> Value,
encode: @escaping @Sendable (Value) throws -> Data
) {
@Dependency(\.defaultFileStorage) var storage
self.storage = storage
self.url = url
self.decode = decode
self.encode = encode
}

public func load(initialValue: Value?) -> Value? {
do {
return try JSONDecoder().decode(Value.self, from: self.storage.load(self.url))
return try decode(self.storage.load(self.url))
} catch {
return initialValue
}
Expand All @@ -51,7 +86,7 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
self.state.withValue { state in
if state.workItem == nil {
self.isSetting.setValue(true)
try? self.storage.save(JSONEncoder().encode(value), self.url)
try? self.storage.save(encode(value), self.url)
let workItem = DispatchWorkItem { [weak self] in
guard let self else { return }
self.state.withValue { state in
Expand All @@ -62,7 +97,7 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, Se
guard let value = state.value
else { return }
self.isSetting.setValue(true)
try? self.storage.save(JSONEncoder().encode(value), self.url)
try? self.storage.save(self.encode(value), self.url)
}
}
state.workItem = workItem
Expand Down
28 changes: 28 additions & 0 deletions Tests/ComposableArchitectureTests/FileStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,21 @@ final class FileStorageTests: XCTestCase {
}
}

func testBasics_CustomDecodeEncodeClosures() {
let fileSystem = LockIsolated<[URL: Data]>([:])
withDependencies {
$0.defaultFileStorage = .inMemory(fileSystem: fileSystem, scheduler: .immediate)
} operation: {
@Shared(.utf8String) var string = ""
XCTAssertNoDifference(fileSystem.value, [.utf8StringURL: Data()])
string = "hello"
XCTAssertNoDifference(
fileSystem.value[.utf8StringURL].map { String(decoding: $0, as: UTF8.self) },
"hello"
)
}
}

func testThrottle() throws {
let fileSystem = LockIsolated<[URL: Data]>([:])
let testScheduler = DispatchQueue.test
Expand Down Expand Up @@ -464,13 +479,26 @@ final class FileStorageTests: XCTestCase {
}
}

extension PersistenceReaderKey
where Self == FileStorageKey<String> {
fileprivate static var utf8String: Self {
.fileStorage(
.utf8StringURL,
decode: { data in String(decoding: data, as: UTF8.self) },
encode: { string in Data(string.utf8) }
)
}
}

extension URL {
fileprivate static let fileURL = Self(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("file.json")
fileprivate static let userURL = Self(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("user.json")
fileprivate static let anotherFileURL = Self(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("another-file.json")
fileprivate static let utf8StringURL = Self(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("utf8-string.json")
}

private struct User: Codable, Equatable, Identifiable {
Expand Down

0 comments on commit 1b627dc

Please sign in to comment.