Skip to content

Commit a6d9211

Browse files
authored
Merge pull request #29 from stackotter/main
2 parents 0796f7e + 3a7f8f9 commit a6d9211

File tree

5 files changed

+135
-18
lines changed

5 files changed

+135
-18
lines changed

Sources/TOMLKit/Decoder/InternalTOMLDecoder.swift

+16-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ final class InternalTOMLDecoder: Decoder {
1212
var dataDecoder: (TOMLValueConvertible) -> Data?
1313
var strictDecoding: Bool = false
1414
var notDecodedKeys: NotDecodedKeys
15+
let originalNotDecodedKeys: [String: [CodingKey]]
1516

1617
let tomlValue: TOMLValue
1718
init(
@@ -28,6 +29,7 @@ final class InternalTOMLDecoder: Decoder {
2829
self.dataDecoder = dataDecoder
2930
self.strictDecoding = strictDecoding
3031
self.notDecodedKeys = notDecodedKeys
32+
self.originalNotDecodedKeys = notDecodedKeys.keys
3133
}
3234

3335
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key: CodingKey {
@@ -42,6 +44,10 @@ final class InternalTOMLDecoder: Decoder {
4244
)
4345
}
4446

47+
// Assume that any previous container creation was related to a failed decoding
48+
// attempt, and reset the not-decoded keys back to the value that the parent
49+
// decoder passed to us.
50+
self.notDecodedKeys.keys = self.originalNotDecodedKeys
4551
return KeyedDecodingContainer<Key>(
4652
KDC(
4753
table: table,
@@ -64,6 +70,11 @@ final class InternalTOMLDecoder: Decoder {
6470
)
6571
)
6672
}
73+
74+
// Assume that any previous container creation was related to a failed decoding
75+
// attempt, and reset the not-decoded keys back to the value that the parent
76+
// decoder passed to us.
77+
self.notDecodedKeys.keys = self.originalNotDecodedKeys
6778
return UDC(
6879
array,
6980
codingPath: self.codingPath,
@@ -75,7 +86,11 @@ final class InternalTOMLDecoder: Decoder {
7586
}
7687

7788
func singleValueContainer() throws -> SingleValueDecodingContainer {
78-
SVDC(
89+
// Assume that any previous container creation was related to a failed decoding
90+
// attempt, and reset the not-decoded keys back to the value that the parent
91+
// decoder passed to us.
92+
self.notDecodedKeys.keys = self.originalNotDecodedKeys
93+
return SVDC(
7994
self.tomlValue,
8095
codingPath: self.codingPath,
8196
dataDecoder: self.dataDecoder,

Sources/TOMLKit/Decoder/KeyedDecodingContainerProtocol.swift

+13-2
Original file line numberDiff line numberDiff line change
@@ -228,13 +228,24 @@ extension InternalTOMLDecoder.KDC {
228228
codingPath: self.codingPath + key,
229229
dataDecoder: self.dataDecoder,
230230
strictDecoding: self.strictDecoding,
231-
notDecodedKeys: self.notDecodedKeys
231+
notDecodedKeys: InternalTOMLDecoder.NotDecodedKeys()
232232
)
233233

234234
self.decodedKeys.append(key.stringValue)
235235

236-
return try T(from: decoder)
236+
let decodedValue = try T(from: decoder)
237+
238+
// Only propagate not-decoded keys if the decoding was successful.
239+
// Otherwise `Decodable` implementations that attempt multiple
240+
// decoding strategies in succession (trying the next if the
241+
// previous one failed), don't work.
242+
for (key, path) in decoder.notDecodedKeys.keys {
243+
self.notDecodedKeys.keys[key] = path
244+
}
245+
246+
return decodedValue
237247
}
248+
238249
}
239250
}
240251

Sources/TOMLKit/Decoder/SingleValueDecodingContainer.swift

+14-3
Original file line numberDiff line numberDiff line change
@@ -213,20 +213,31 @@ extension InternalTOMLDecoder.SVDC {
213213
codingPath: self.codingPath,
214214
dataDecoder: self.dataDecoder,
215215
strictDecoding: self.strictDecoding,
216-
notDecodedKeys: self.notDecodedKeys
216+
notDecodedKeys: InternalTOMLDecoder.NotDecodedKeys()
217217
)
218218

219+
let decodedValue: T
219220
if type is Data.Type {
220221
if let data = self.dataDecoder(self.tomlValue) {
221-
return data as! T
222+
decodedValue = data as! T
222223
} else {
223224
throw DecodingError.dataCorrupted(DecodingError.Context(
224225
codingPath: self.codingPath,
225226
debugDescription: "Unable to decode Data from: \"\(self.tomlValue.debugDescription)\"."
226227
))
227228
}
228229
} else {
229-
return try T(from: decoder)
230+
decodedValue = try T(from: decoder)
230231
}
232+
233+
// Only propagate not-decoded keys if the decoding was successful.
234+
// Otherwise `Decodable` implementations that attempt multiple
235+
// decoding strategies in succession (trying the next if the
236+
// previous one failed), don't work.
237+
for (key, path) in decoder.notDecodedKeys.keys {
238+
self.notDecodedKeys.keys[key] = path
239+
}
240+
241+
return decodedValue
231242
}
232243
}

Sources/TOMLKit/Decoder/UnkeyedDecodingContainer.swift

+13-3
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,21 @@ extension InternalTOMLDecoder.UDC {
166166
codingPath: self.codingPath + TOMLCodingKey(index: self.currentIndex),
167167
dataDecoder: self.dataDecoder,
168168
strictDecoding: self.strictDecoding,
169-
notDecodedKeys: self.notDecodedKeys
169+
notDecodedKeys: InternalTOMLDecoder.NotDecodedKeys()
170170
)
171-
let decodable = try T(from: decoder)
171+
172+
let decodedValue = try T(from: decoder)
172173
self.currentIndex += 1
173-
return decodable
174+
175+
// Only propagate not-decoded keys if the decoding was successful.
176+
// Otherwise `Decodable` implementations that attempt multiple
177+
// decoding strategies in succession (trying the next if the
178+
// previous one failed), don't work.
179+
for (key, path) in decoder.notDecodedKeys.keys {
180+
self.notDecodedKeys.keys[key] = path
181+
}
182+
183+
return decodedValue
174184
}
175185
}
176186

Tests/TOMLKitTests/TOMLKitTests.swift

+79-9
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,6 @@ enum CodableEnum: String, Codable, Equatable {
1919
case abc
2020
case def
2121
case ghi
22-
23-
public static func == (lhs: Self, rhs: Self) -> Bool {
24-
switch (lhs, rhs) {
25-
case (.abc, .abc): return true
26-
case (.def, .def): return true
27-
case (.ghi, .ghi): return true
28-
default: return false
29-
}
30-
}
3122
}
3223

3324
struct CodableStruct: Codable, Equatable {
@@ -501,4 +492,83 @@ final class TOMLKitTests: XCTestCase {
501492
let _ = try udc.nestedUnkeyedContainer()
502493
XCTAssertEqual(udc.currentIndex, 1)
503494
}
495+
496+
// Tests for a bug where not-decoded keys would carry over from unsuccessful
497+
// decoding attempts, breaking the common 'try one thing then retry another'
498+
// decoding pattern. This bug was specific to decoders with
499+
// `strictDecoding: true`.
500+
func testRetryKeyedOrDictionary() throws {
501+
enum StructOrDictionary: Decodable, Equatable {
502+
case `struct`(value: Int)
503+
case dictionary([String: Int])
504+
505+
enum CodingKeys: String, CodingKey {
506+
case value
507+
}
508+
509+
init(from decoder: Decoder) throws {
510+
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
511+
if let value = try? container.decode(Int.self, forKey: .value) {
512+
self = .struct(value: value)
513+
}
514+
}
515+
516+
if let container = try? decoder.singleValueContainer() {
517+
self = try .dictionary(container.decode([String: Int].self))
518+
} else {
519+
throw DecodingError.dataCorrupted(.init(
520+
codingPath: [],
521+
debugDescription: "Expected int or keyedInt"
522+
))
523+
}
524+
}
525+
}
526+
527+
let toml = "other_value = 1"
528+
529+
// Before the bug got fixed, this line would throw an unused key error.
530+
let value = try TOMLDecoder(strictDecoding: true).decode(StructOrDictionary.self, from: toml)
531+
532+
// We don't actually expect this to fail, it's not the point of the test, but
533+
// might as well assert it just in case.
534+
XCTAssertEqual(value, .dictionary(["other_value": 1]))
535+
}
536+
537+
// This is related to the test above, but tests for a different aspect of the
538+
// bug (I originally fixed one but not the other so they are kind of independent).
539+
func testRetryKeyedOrInt() throws {
540+
struct SimpleCodableStruct: Codable, Equatable {
541+
var value: Int
542+
}
543+
544+
enum SimpleOrComplex: Decodable, Equatable {
545+
case simple(SimpleCodableStruct)
546+
case complex(CodableStruct)
547+
548+
init(from decoder: Decoder) throws {
549+
if let container = try? decoder.singleValueContainer() {
550+
if let value = try? container.decode(CodableStruct.self) {
551+
self = .complex(value)
552+
return
553+
} else if let value = try? container.decode(SimpleCodableStruct.self) {
554+
self = .simple(value)
555+
return
556+
}
557+
}
558+
throw DecodingError.dataCorrupted(.init(
559+
codingPath: [],
560+
debugDescription: "Expected CodableStruct or SimpleCodableStruct"
561+
))
562+
}
563+
}
564+
565+
let toml = "value = 2"
566+
567+
// Before the bug got fixed, this line would throw an unused key error.
568+
let value = try TOMLDecoder(strictDecoding: true).decode(SimpleOrComplex.self, from: toml)
569+
570+
// We don't actually expect this to fail, it's not the point of the test, but
571+
// might as well assert it just in case.
572+
XCTAssertEqual(value, .simple(SimpleCodableStruct(value: 2)))
573+
}
504574
}

0 commit comments

Comments
 (0)