Skip to content

Commit e47f778

Browse files
fatbobmanclaude
andcommitted
Fix RawRepresentable & Codable conflict (issue #23)
This commit resolves the compilation ambiguity that occurred when types conform to both RawRepresentable and Codable protocols. Changes: - Add @_disfavoredOverload to Codable methods in UserDefaultsWrapper - Add @_disfavoredOverload to Codable methods in NSUbiquitousKeyValueStoreWrapper - Add comprehensive tests for RawRepresentable & Codable types (UserDefaults + Cloud) - Update documentation to reflect that RawRepresentable takes priority over Codable When a type conforms to both protocols, the library now prioritizes the RawRepresentable storage method, ensuring backward compatibility with existing data and more efficient storage for enum types. Fixes #23 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e761edb commit e47f778

6 files changed

Lines changed: 267 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ class AppearanceSettings {
436436
}
437437
```
438438

439-
Because these enums are handled through `RawRepresentable`, avoid adding an explicit `Codable` conformance to the same type—doing so can introduce ambiguous overloads in the generated storage accessors. If you need custom encoding, wrap the enum in another `Codable` type instead of conforming the enum itself.
439+
When a type conforms to both `RawRepresentable` and `Codable`, the library will prioritize the `RawRepresentable` storage method, storing values using their raw representation rather than JSON encoding. This ensures backward compatibility with existing data and provides more efficient storage for enum types.
440440

441441
### Integrating with Other Observable Objects
442442

README_zh.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ class AppearanceSettings {
436436
}
437437
```
438438

439-
由于这些枚举依赖 `RawRepresentable` 存储,请避免为它们额外声明 `Codable`,否则会在生成的存取器中出现重载歧义。如果需要自定义编码,可在更高层的 `Codable` 模型中包装该枚举,而不是直接让枚举本身符合 `Codable`
439+
当类型同时遵循 `RawRepresentable` `Codable` 时,库会优先使用 `RawRepresentable` 的存储方式,通过原始值(raw value)存储数据,而不是使用 JSON 编码。这确保了与现有数据的向后兼容性,并为枚举类型提供了更高效的存储方式
440440

441441
### 与其他 Observable 对象集成
442442

Sources/ObservableDefaults/NSUbiquitousKeyValueStore/NSUbiquitousKeyValueStoreWrapper.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ public struct NSUbiquitousKeyValueStoreWrapper: Sendable {
147147
/// - defaultValue: The default value to return if the key is not found or decoding fails
148148
/// - Returns: The decoded value from the store, or the default value if the key is not found or
149149
/// decoding fails
150+
@_disfavoredOverload
150151
public func getValue<Value>(
151152
_ key: String,
152153
_ defaultValue: Value) -> Value
@@ -172,6 +173,7 @@ public struct NSUbiquitousKeyValueStoreWrapper: Sendable {
172173
/// - defaultValue: The default optional value to return if the key is not found or decoding fails
173174
/// - Returns: The decoded optional value from the store, or the default value if the key is not found or
174175
/// decoding fails
176+
@_disfavoredOverload
175177
public func getValue<Value>(
176178
_ key: String,
177179
_ defaultValue: Value?) -> Value?
@@ -309,6 +311,7 @@ public struct NSUbiquitousKeyValueStoreWrapper: Sendable {
309311
/// - key: The key to set the value for
310312
/// - newValue: The new Codable value to store
311313
/// - Note: If encoding fails, the method silently returns without storing anything
314+
@_disfavoredOverload
312315
public func setValue<Value: Codable>(
313316
_ key: String,
314317
_ newValue: Value) {
@@ -324,6 +327,7 @@ public struct NSUbiquitousKeyValueStoreWrapper: Sendable {
324327
/// - key: The key to set the value for
325328
/// - newValue: The new optional Codable value to store (nil will remove the key)
326329
/// - Note: If encoding fails, the method silently returns without storing anything
330+
@_disfavoredOverload
327331
public func setValue<Value: Codable>(
328332
_ key: String,
329333
_ newValue: Value?)

Sources/ObservableDefaults/UserDefaults/UserDefaultsWrapper.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ public struct UserDefaultsWrapper<Value> {
171171
/// - store: The user defaults store to get the value from
172172
/// - Returns: The decoded value from the user defaults store, or the default value if the key
173173
/// is not found or decoding fails
174+
@_disfavoredOverload
174175
public nonisolated static func getValue(
175176
_ key: String,
176177
_ defaultValue: Value,
@@ -198,6 +199,7 @@ public struct UserDefaultsWrapper<Value> {
198199
/// - store: The user defaults store to get the value from
199200
/// - Returns: The decoded optional value from the user defaults store, or the default value if the key
200201
/// is not found or decoding fails
202+
@_disfavoredOverload
201203
public nonisolated static func getValue(
202204
_ key: String,
203205
_ defaultValue: Value?,
@@ -307,6 +309,7 @@ public struct UserDefaultsWrapper<Value> {
307309
/// - newValue: The new Codable value to store
308310
/// - store: The user defaults store to set the value in
309311
/// - Note: If encoding fails, the method silently returns without storing anything
312+
@_disfavoredOverload
310313
public nonisolated static func setValue(_ key: String, _ newValue: Value, _ store: UserDefaults)
311314
where Value: Codable {
312315
// Encode the value to JSON data
@@ -322,6 +325,7 @@ public struct UserDefaultsWrapper<Value> {
322325
/// - newValue: The new optional Codable value to store (nil will remove the key)
323326
/// - store: The user defaults store to set the value in
324327
/// - Note: If encoding fails, the method silently returns without storing anything
328+
@_disfavoredOverload
325329
public nonisolated static func setValue(_ key: String, _ newValue: Value?, _ store: UserDefaults)
326330
where Value: Codable {
327331
guard let value = newValue else {
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import Foundation
2+
import ObservableDefaults
3+
import Testing
4+
5+
// Test type that conforms to both RawRepresentable and Codable
6+
// This simulates the user's scenario from issue #23
7+
private struct UserProfile: RawRepresentable, Equatable {
8+
var name: String
9+
var age: Int
10+
11+
// RawRepresentable conformance using JSON string
12+
var rawValue: String {
13+
// Manually construct JSON to avoid Codable encoding
14+
return "{\"name\": \"\(name)\", \"age\": \(age)}"
15+
}
16+
17+
init?(rawValue: String) {
18+
// Simple JSON parsing for test purposes
19+
guard let data = rawValue.data(using: .utf8),
20+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
21+
let name = json["name"] as? String,
22+
let age = json["age"] as? Int else {
23+
return nil
24+
}
25+
self.name = name
26+
self.age = age
27+
}
28+
29+
init(name: String, age: Int) {
30+
self.name = name
31+
self.age = age
32+
}
33+
}
34+
35+
// Make it Codable for testing the conflict scenario
36+
extension UserProfile: Codable {
37+
enum CodingKeys: String, CodingKey {
38+
case name
39+
case age
40+
}
41+
}
42+
43+
// Optional version test
44+
private enum Status: String, Codable {
45+
case active
46+
case inactive
47+
}
48+
49+
@ObservableDefaults(prefix: "Test1_")
50+
private class TestStore1 {
51+
var profile = UserProfile(name: "Default", age: 0)
52+
}
53+
54+
@ObservableDefaults(prefix: "Test2_")
55+
private class TestStore2 {
56+
var profile = UserProfile(name: "Default", age: 0)
57+
}
58+
59+
@ObservableDefaults(prefix: "Test3_")
60+
private class TestStore3 {
61+
var optionalProfile: UserProfile?
62+
}
63+
64+
@ObservableDefaults(prefix: "Test4_")
65+
private class TestStore4 {
66+
var status = Status.active
67+
}
68+
69+
@ObservableCloud(prefix: "CloudTest1_", developmentMode: true)
70+
private class CloudTestStore1 {
71+
var profile = UserProfile(name: "Default", age: 0)
72+
}
73+
74+
@ObservableCloud(prefix: "CloudTest2_", developmentMode: true)
75+
private class CloudTestStore2 {
76+
var profile = UserProfile(name: "Default", age: 0)
77+
}
78+
79+
@ObservableCloud(prefix: "CloudTest3_", developmentMode: true)
80+
private class CloudTestStore3 {
81+
var optionalProfile: UserProfile?
82+
}
83+
84+
@ObservableCloud(prefix: "CloudTest4_", developmentMode: true)
85+
private class CloudTestStore4 {
86+
var status = Status.active
87+
}
88+
89+
@Suite("RawRepresentable & Codable Conflict Tests")
90+
struct RawRepresentableCodableTests {
91+
92+
@Test("Type conforming to both RawRepresentable and Codable should compile")
93+
func compilationTest() {
94+
// This test verifies that the code compiles without ambiguity errors
95+
let store = TestStore1()
96+
#expect(store.profile.name == "Default")
97+
#expect(store.profile.age == 0)
98+
}
99+
100+
@Test("Should use RawRepresentable storage (not Codable JSON encoding)")
101+
func storageFormatTest() {
102+
let store = TestStore2()
103+
let testProfile = UserProfile(name: "Alice", age: 30)
104+
105+
store.profile = testProfile
106+
107+
// Verify the value is stored correctly
108+
#expect(store.profile.name == "Alice")
109+
#expect(store.profile.age == 30)
110+
111+
// Check that it's stored as a String (RawRepresentable way), not as Data (Codable way)
112+
let userDefaults = UserDefaults.standard
113+
let storedValue = userDefaults.object(forKey: "Test2_profile")
114+
115+
// Should be stored as String (raw value), not Data (Codable encoding)
116+
#expect(storedValue is String)
117+
#expect(storedValue is Data == false)
118+
}
119+
120+
@Test("Should read existing RawRepresentable data correctly")
121+
func backwardCompatibilityTest() {
122+
let userDefaults = UserDefaults.standard
123+
let key = "testProfile"
124+
125+
// Simulate existing data stored as RawRepresentable (JSON string)
126+
let existingProfile = UserProfile(name: "Bob", age: 25)
127+
userDefaults.set(existingProfile.rawValue, forKey: key)
128+
129+
// Read it back using the wrapper
130+
let retrieved = UserDefaultsWrapper<UserProfile>.getValue(
131+
key,
132+
UserProfile(name: "Default", age: 0),
133+
userDefaults
134+
)
135+
136+
#expect(retrieved.name == "Bob")
137+
#expect(retrieved.age == 25)
138+
139+
// Cleanup
140+
userDefaults.removeObject(forKey: key)
141+
}
142+
143+
@Test("Optional RawRepresentable & Codable type should work")
144+
func optionalTest() {
145+
let store = TestStore3()
146+
147+
#expect(store.optionalProfile == nil)
148+
149+
store.optionalProfile = UserProfile(name: "Charlie", age: 35)
150+
#expect(store.optionalProfile?.name == "Charlie")
151+
#expect(store.optionalProfile?.age == 35)
152+
153+
store.optionalProfile = nil
154+
#expect(store.optionalProfile == nil)
155+
}
156+
157+
@Test("Enum with RawRepresentable and Codable should work")
158+
func enumTest() {
159+
// Clean up to ensure fresh state
160+
UserDefaults.standard.removeObject(forKey: "Test4_status")
161+
162+
let store = TestStore4()
163+
164+
#expect(store.status == .active)
165+
166+
store.status = .inactive
167+
#expect(store.status == .inactive)
168+
169+
// Verify it's stored as raw value (String), not encoded Data
170+
let userDefaults = UserDefaults.standard
171+
let storedValue = userDefaults.object(forKey: "Test4_status")
172+
#expect(storedValue is String)
173+
#expect((storedValue as? String) == "inactive")
174+
}
175+
176+
// MARK: - Cloud Tests
177+
178+
@Test("Cloud: Type conforming to both RawRepresentable and Codable should compile")
179+
func cloudCompilationTest() {
180+
let store = CloudTestStore1()
181+
#expect(store.profile.name == "Default")
182+
#expect(store.profile.age == 0)
183+
}
184+
185+
@Test("Cloud: Should use RawRepresentable storage (not Codable JSON encoding)")
186+
func cloudStorageFormatTest() {
187+
let store = CloudTestStore2()
188+
let testProfile = UserProfile(name: "Alice", age: 30)
189+
190+
store.profile = testProfile
191+
192+
#expect(store.profile.name == "Alice")
193+
#expect(store.profile.age == 30)
194+
}
195+
196+
@Test("Cloud: Optional RawRepresentable & Codable type should work")
197+
func cloudOptionalTest() {
198+
let store = CloudTestStore3()
199+
200+
#expect(store.optionalProfile == nil)
201+
202+
store.optionalProfile = UserProfile(name: "Charlie", age: 35)
203+
#expect(store.optionalProfile?.name == "Charlie")
204+
#expect(store.optionalProfile?.age == 35)
205+
206+
store.optionalProfile = nil
207+
#expect(store.optionalProfile == nil)
208+
}
209+
210+
@Test("Cloud: Enum with RawRepresentable and Codable should work")
211+
func cloudEnumTest() {
212+
let store = CloudTestStore4()
213+
214+
#expect(store.status == .active)
215+
216+
store.status = .inactive
217+
#expect(store.status == .inactive)
218+
}
219+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Foundation
2+
import ObservableDefaults
3+
import Testing
4+
5+
// Minimal test to verify compilation works
6+
private struct TestType: RawRepresentable, Codable, Equatable {
7+
var value: String
8+
9+
var rawValue: String {
10+
value
11+
}
12+
13+
init?(rawValue: String) {
14+
self.value = rawValue
15+
}
16+
17+
init(value: String) {
18+
self.value = value
19+
}
20+
}
21+
22+
@ObservableDefaults(prefix: "SimpleTest_")
23+
private class SimpleStore {
24+
var test = TestType(value: "default")
25+
}
26+
27+
@Suite("Simple RawRepresentable & Codable Test")
28+
struct SimpleRawRepresentableCodableTest {
29+
@Test
30+
func verifyCompilation() {
31+
// Clean up UserDefaults before test
32+
UserDefaults.standard.removeObject(forKey: "SimpleTest_test")
33+
34+
let store = SimpleStore()
35+
store.test = TestType(value: "updated")
36+
#expect(store.test.value == "updated")
37+
}
38+
}

0 commit comments

Comments
 (0)