diff --git a/.gitignore b/.gitignore index 8360953a99..e54808a55b 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ Package.resolved *.o *.d *.swiftdeps* +*.dia # CocoaPods # diff --git a/Sources/PluginCore/Variables/Enum/Switcher/TaggedEnumSwitcherVariable.swift b/Sources/PluginCore/Variables/Enum/Switcher/TaggedEnumSwitcherVariable.swift index f97bb45f5d..6c461d1b91 100644 --- a/Sources/PluginCore/Variables/Enum/Switcher/TaggedEnumSwitcherVariable.swift +++ b/Sources/PluginCore/Variables/Enum/Switcher/TaggedEnumSwitcherVariable.swift @@ -43,11 +43,20 @@ extension TaggedEnumSwitcherVariable { let values = value.decodeExprs .filter { $0.type == header.type } .map(\.syntax) - let cLocation = EnumCaseCodingLocation( - coder: coder, values: values - ) - let generated = `case`.decoding(in: context, from: cLocation) + // Only generate decoding syntax for cases that match the + // current type being considered. Doing so for non-matching + // cases is not only wasteful but also mutates shared per-case + // node state (e.g. the cached container name), which would + // suppress container generation for the type that actually + // matches, leading to values being decoded from the wrong + // container. if !values.isEmpty { + let cLocation = EnumCaseCodingLocation( + coder: coder, values: values + ) + let generated = `case`.decoding( + in: context, from: cLocation + ) let _ = { switchable = true }() SwitchCaseSyntax(label: .case(generated.label)) { preSyntax("\(values.first!)") diff --git a/Tests/MetaCodableTests/Codable/CommonStrategiesValueCoderTests.swift b/Tests/MetaCodableTests/Codable/CommonStrategiesValueCoderTests.swift index 14e8df7948..af71d39f1a 100644 --- a/Tests/MetaCodableTests/Codable/CommonStrategiesValueCoderTests.swift +++ b/Tests/MetaCodableTests/Codable/CommonStrategiesValueCoderTests.swift @@ -566,16 +566,19 @@ struct CommonStrategiesValueCoderTests { switch typeString { case "active": let since: String + let container = try decoder.container(keyedBy: CodingKeys.self) since = try ValueCoder().decode(from: container, forKey: CodingKeys.since) self = .active(since: since) return case "inactive": let reason: String + let container = try decoder.container(keyedBy: CodingKeys.self) reason = try ValueCoder().decode(from: container, forKey: CodingKeys.reason) self = .inactive(reason: reason) return case "pending": let until: String + let container = try decoder.container(keyedBy: CodingKeys.self) until = try ValueCoder().decode(from: container, forKey: CodingKeys.until) self = .pending(until: until) return diff --git a/Tests/MetaCodableTests/CodedAs/CodedAsEnumTests.swift b/Tests/MetaCodableTests/CodedAs/CodedAsEnumTests.swift index b8a3576684..fb54684f2e 100644 --- a/Tests/MetaCodableTests/CodedAs/CodedAsEnumTests.swift +++ b/Tests/MetaCodableTests/CodedAs/CodedAsEnumTests.swift @@ -86,12 +86,14 @@ struct CodedAsEnumTests { switch typeString { case "load", true ... false: let key: String + let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: CodingKeys.key) self = .load(key: key) return case "store", 1 ... "end": let key: String let value: Int + let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: CodingKeys.key) value = try container.decode(Int.self, forKey: CodingKeys.value) self = .store(key: key, value: value) @@ -590,11 +592,13 @@ struct CodedAsEnumTests { switch typeString { case "bool": let variable: Bool + let container = try decoder.container(keyedBy: CodingKeys.self) variable = try container.decode(Bool.self, forKey: CodingKeys.variable) self = .bool(_: variable) return case "altInt": let val: Int + let container = try decoder.container(keyedBy: CodingKeys.self) val = try container.decode(Int.self, forKey: CodingKeys.val) self = .int(val: val) return @@ -612,6 +616,7 @@ struct CodedAsEnumTests { let variable: Bool let val: Int let _2: String + let container = try decoder.container(keyedBy: CodingKeys.self) _2 = try String(from: decoder) variable = try container.decode(Bool.self, forKey: CodingKeys.variable) val = try container.decode(Int.self, forKey: CodingKeys.val) diff --git a/Tests/MetaCodableTests/CodedAt/CodedAtEnumTests.swift b/Tests/MetaCodableTests/CodedAt/CodedAtEnumTests.swift index f5d7cc4734..3358aa38dd 100644 --- a/Tests/MetaCodableTests/CodedAt/CodedAtEnumTests.swift +++ b/Tests/MetaCodableTests/CodedAt/CodedAtEnumTests.swift @@ -84,11 +84,13 @@ struct CodedAtEnumTests { switch typeString { case "bool": let variable: Bool + let container = try decoder.container(keyedBy: CodingKeys.self) variable = try container.decode(Bool.self, forKey: CodingKeys.variable) self = .bool(_: variable) return case "int": let val: Int + let container = try decoder.container(keyedBy: CodingKeys.self) val = try container.decode(Int.self, forKey: CodingKeys.val) self = .int(val: val) return @@ -101,6 +103,7 @@ struct CodedAtEnumTests { let variable: Bool let val: Int let _2: String + let container = try decoder.container(keyedBy: CodingKeys.self) _2 = try String(from: decoder) variable = try container.decode(Bool.self, forKey: CodingKeys.variable) val = try container.decode(Int.self, forKey: CodingKeys.val) @@ -222,12 +225,14 @@ struct CodedAtEnumTests { switch typeString { case "load": let key: String + let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: CodingKeys.key) self = .load(key: key) return case "store": let key: String let value: Int + let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: CodingKeys.key) value = try container.decode(Int.self, forKey: CodingKeys.value) self = .store(key: key, value: value) diff --git a/Tests/MetaCodableTests/ContentAtTests.swift b/Tests/MetaCodableTests/ContentAtTests.swift index 44f9689483..e4d4392d24 100644 --- a/Tests/MetaCodableTests/ContentAtTests.swift +++ b/Tests/MetaCodableTests/ContentAtTests.swift @@ -50,7 +50,7 @@ struct ContentAtTests { @Codable @CodedAt("type") @ContentAt("content") - enum Command { + enum Command: Equatable { case load(key: String) case store(key: String, value: Int) } @@ -91,12 +91,14 @@ struct ContentAtTests { switch typeString { case "load": let key: String + let container = try contentDecoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: CodingKeys.key) self = .load(key: key) return case "store": let key: String let value: Int + let container = try contentDecoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: CodingKeys.key) value = try container.decode(Int.self, forKey: CodingKeys.value) self = .store(key: key, value: value) @@ -160,12 +162,15 @@ struct ContentAtTests { @Test func contentAtFromJSON() throws { - // The decoding expects key/value at root level, not in content + // Associated values are nested under the content key, mirroring + // the structure produced by the encoder. let jsonStr = """ { "type": "store", - "key": "my_key", - "value": 42 + "content": { + "key": "my_key", + "value": 42 + } } """ let jsonData = try #require(jsonStr.data(using: .utf8)) @@ -192,6 +197,23 @@ struct ContentAtTests { #expect(content["key"] as? String == "test") #expect(content["value"] as? Int == 100) } + + @Test + func contentAtRoundTrip() throws { + // Reproduces https://github.com/SwiftyLab/MetaCodable/issues/163 + // The encoder nests associated values under the content key, so a + // round-trip must be able to decode them back from that same key. + for command: Command in [ + .load(key: "my_key"), + .store(key: "my_key", value: 42), + ] { + let data = try JSONEncoder().encode(command) + let decoded = try JSONDecoder().decode( + Command.self, from: data + ) + #expect(decoded == command) + } + } } struct WithExplicitType { diff --git a/Tests/MetaCodableTests/DecodedAtEncodedAtIntegrationTests.swift b/Tests/MetaCodableTests/DecodedAtEncodedAtIntegrationTests.swift index 3cff450419..23851c0dcd 100644 --- a/Tests/MetaCodableTests/DecodedAtEncodedAtIntegrationTests.swift +++ b/Tests/MetaCodableTests/DecodedAtEncodedAtIntegrationTests.swift @@ -413,12 +413,14 @@ struct DecodedAtEncodedAtIntegrationTests { switch typeString { case "load": let key: String + let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: CodingKeys.key) self = .load(key: key) return case "store": let key: String let value: Int + let container = try decoder.container(keyedBy: CodingKeys.self) key = try container.decode(String.self, forKey: CodingKeys.key) value = try container.decode(Int.self, forKey: CodingKeys.value) self = .store(key: key, value: value)