Add CustomCodablePreferenceKey

This commit is contained in:
Shadowfacts 2024-04-15 10:50:08 -04:00
parent 70227a7fa1
commit e5c4fceacd
3 changed files with 54 additions and 12 deletions

View File

@ -27,17 +27,26 @@ final class Preference<Key: PreferenceKey>: Codable {
} }
init(from decoder: any Decoder) throws { init(from decoder: any Decoder) throws {
if let container = try? decoder.singleValueContainer() { if let keyType = Key.self as? any CustomCodablePreferenceKey.Type {
self.storedValue = try keyType.decode(from: decoder) as! Key.Value?
} else if let container = try? decoder.singleValueContainer() {
self.storedValue = try? container.decode(Key.Value.self) self.storedValue = try? container.decode(Key.Value.self)
} }
} }
func encode(to encoder: any Encoder) throws { func encode(to encoder: any Encoder) throws {
if let storedValue { if let storedValue {
if let keyType = Key.self as? any CustomCodablePreferenceKey.Type {
func encode<K: CustomCodablePreferenceKey>(_: K.Type) throws {
try K.encode(value: storedValue as! K.Value, to: encoder)
}
return try _openExistential(keyType, do: encode)
} else {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
try container.encode(storedValue) try container.encode(storedValue)
} }
} }
}
static subscript( static subscript(
_enclosingInstance instance: PreferenceStore, _enclosingInstance instance: PreferenceStore,

View File

@ -28,3 +28,8 @@ extension MigratablePreferenceKey {
oldValue != defaultValue oldValue != defaultValue
} }
} }
protocol CustomCodablePreferenceKey: PreferenceKey {
static func encode(value: Value, to encoder: any Encoder) throws
static func decode(from decoder: any Decoder) throws -> Value?
}

View File

@ -15,11 +15,11 @@ final class PreferenceStoreTests: XCTestCase {
static let defaultValue = false static let defaultValue = false
} }
final class TestStore: Codable, ObservableObject { final class TestStore<Key: PreferenceKey>: Codable, ObservableObject {
private var _test = Preference<TestKey>() private var _test = Preference<Key>()
// the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually // the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually
var test: Bool { var test: Key.Value {
get { get {
Preference.get(enclosingInstance: self, storage: \._test) Preference.get(enclosingInstance: self, storage: \._test)
} }
@ -28,7 +28,7 @@ final class PreferenceStoreTests: XCTestCase {
} }
} }
var testPublisher: some Publisher<TestKey.Value, Never> { var testPublisher: some Publisher<Key.Value, Never> {
_test.projectedValue _test.projectedValue
} }
@ -37,7 +37,7 @@ final class PreferenceStoreTests: XCTestCase {
init(from decoder: any Decoder) throws { init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self) let container = try decoder.container(keyedBy: CodingKeys.self)
self._test = try container.decode(Preference<TestKey>.self, forKey: .test) self._test = try container.decode(Preference<Key>.self, forKey: .test)
} }
enum CodingKeys: CodingKey { enum CodingKeys: CodingKey {
@ -52,18 +52,18 @@ final class PreferenceStoreTests: XCTestCase {
func testDecoding() throws { func testDecoding() throws {
let decoder = JSONDecoder() let decoder = JSONDecoder()
let present = try decoder.decode(PreferenceCoding<TestStore>.self, from: Data(""" let present = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
{"test": true} {"test": true}
""".utf8)).wrapped """.utf8)).wrapped
XCTAssertEqual(present.test, true) XCTAssertEqual(present.test, true)
let absent = try decoder.decode(PreferenceCoding<TestStore>.self, from: Data(""" let absent = try decoder.decode(PreferenceCoding<TestStore<TestKey>>.self, from: Data("""
{} {}
""".utf8)).wrapped """.utf8)).wrapped
XCTAssertEqual(absent.test, false) XCTAssertEqual(absent.test, false)
} }
func testEncoding() throws { func testEncoding() throws {
let store = TestStore() let store = TestStore<TestKey>()
let encoder = JSONEncoder() let encoder = JSONEncoder()
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """ XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
{} {}
@ -83,7 +83,7 @@ final class PreferenceStoreTests: XCTestCase {
let specificPref = expectation(description: "preference publisher") let specificPref = expectation(description: "preference publisher")
// initial and on change // initial and on change
specificPref.expectedFulfillmentCount = 2 specificPref.expectedFulfillmentCount = 2
let store = TestStore() let store = TestStore<TestKey>()
var cancellables = Set<AnyCancellable>() var cancellables = Set<AnyCancellable>()
store.objectWillChange.sink { store.objectWillChange.sink {
topLevel.fulfill() topLevel.fulfill()
@ -97,4 +97,32 @@ final class PreferenceStoreTests: XCTestCase {
wait(for: [topLevel, specificPref]) wait(for: [topLevel, specificPref])
} }
func testCustomCodable() throws {
struct Key: CustomCodablePreferenceKey {
static let defaultValue = 1
static func encode(value: Int, to encoder: any Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(2)
}
static func decode(from decoder: any Decoder) throws -> Int? {
3
}
}
let store = TestStore<Key>()
store.test = 123
let encoder = JSONEncoder()
XCTAssertEqual(String(data: try encoder.encode(PreferenceCoding(wrapped: store)), encoding: .utf8)!, """
{"test":2}
""")
let decoder = JSONDecoder()
let present = try decoder.decode(PreferenceCoding<TestStore<Key>>.self, from: Data("""
{"test":2}
""".utf8)).wrapped
XCTAssertEqual(present.test, 3)
let absent = try decoder.decode(PreferenceCoding<TestStore<Key>>.self, from: Data("""
{}
""".utf8)).wrapped
XCTAssertEqual(absent.test, 1)
}
} }