diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/Preference.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/Preference.swift index 9d96aec8b5..ebaf88c289 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/Preference.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/Preference.swift @@ -27,15 +27,24 @@ final class Preference: Codable { } 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) } } func encode(to encoder: any Encoder) throws { if let storedValue { - var container = encoder.singleValueContainer() - try container.encode(storedValue) + if let keyType = Key.self as? any CustomCodablePreferenceKey.Type { + func encode(_: K.Type) throws { + try K.encode(value: storedValue as! K.Value, to: encoder) + } + return try _openExistential(keyType, do: encode) + } else { + var container = encoder.singleValueContainer() + try container.encode(storedValue) + } } } diff --git a/Packages/TuskerPreferences/Sources/TuskerPreferences/PreferenceKey.swift b/Packages/TuskerPreferences/Sources/TuskerPreferences/PreferenceKey.swift index f0a96f080f..90f82e56f7 100644 --- a/Packages/TuskerPreferences/Sources/TuskerPreferences/PreferenceKey.swift +++ b/Packages/TuskerPreferences/Sources/TuskerPreferences/PreferenceKey.swift @@ -28,3 +28,8 @@ extension MigratablePreferenceKey { oldValue != defaultValue } } + +protocol CustomCodablePreferenceKey: PreferenceKey { + static func encode(value: Value, to encoder: any Encoder) throws + static func decode(from decoder: any Decoder) throws -> Value? +} diff --git a/Packages/TuskerPreferences/Tests/TuskerPreferencesTests/PreferenceStoreTests.swift b/Packages/TuskerPreferences/Tests/TuskerPreferencesTests/PreferenceStoreTests.swift index 1527dbec57..5bd5683576 100644 --- a/Packages/TuskerPreferences/Tests/TuskerPreferencesTests/PreferenceStoreTests.swift +++ b/Packages/TuskerPreferences/Tests/TuskerPreferencesTests/PreferenceStoreTests.swift @@ -15,11 +15,11 @@ final class PreferenceStoreTests: XCTestCase { static let defaultValue = false } - final class TestStore: Codable, ObservableObject { - private var _test = Preference() + final class TestStore: Codable, ObservableObject { + private var _test = Preference() // the acutal subscript expects the enclosingInstance to be a PreferenceStore, so do it manually - var test: Bool { + var test: Key.Value { get { Preference.get(enclosingInstance: self, storage: \._test) } @@ -28,7 +28,7 @@ final class PreferenceStoreTests: XCTestCase { } } - var testPublisher: some Publisher { + var testPublisher: some Publisher { _test.projectedValue } @@ -37,7 +37,7 @@ final class PreferenceStoreTests: XCTestCase { init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - self._test = try container.decode(Preference.self, forKey: .test) + self._test = try container.decode(Preference.self, forKey: .test) } enum CodingKeys: CodingKey { @@ -52,18 +52,18 @@ final class PreferenceStoreTests: XCTestCase { func testDecoding() throws { let decoder = JSONDecoder() - let present = try decoder.decode(PreferenceCoding.self, from: Data(""" + let present = try decoder.decode(PreferenceCoding>.self, from: Data(""" {"test": true} """.utf8)).wrapped XCTAssertEqual(present.test, true) - let absent = try decoder.decode(PreferenceCoding.self, from: Data(""" + let absent = try decoder.decode(PreferenceCoding>.self, from: Data(""" {} """.utf8)).wrapped XCTAssertEqual(absent.test, false) } func testEncoding() throws { - let store = TestStore() + let store = TestStore() let encoder = JSONEncoder() 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") // initial and on change specificPref.expectedFulfillmentCount = 2 - let store = TestStore() + let store = TestStore() var cancellables = Set() store.objectWillChange.sink { topLevel.fulfill() @@ -96,5 +96,33 @@ final class PreferenceStoreTests: XCTestCase { store.test = true 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() + 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>.self, from: Data(""" +{"test":2} +""".utf8)).wrapped + XCTAssertEqual(present.test, 3) + let absent = try decoder.decode(PreferenceCoding>.self, from: Data(""" +{} +""".utf8)).wrapped + XCTAssertEqual(absent.test, 1) + } }