Skip to content

Commit

Permalink
Merge pull request #29 from stackotter/main
Browse files Browse the repository at this point in the history
  • Loading branch information
LebJe authored Jan 18, 2025
2 parents 0796f7e + 3a7f8f9 commit a6d9211
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 18 deletions.
17 changes: 16 additions & 1 deletion Sources/TOMLKit/Decoder/InternalTOMLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class InternalTOMLDecoder: Decoder {
var dataDecoder: (TOMLValueConvertible) -> Data?
var strictDecoding: Bool = false
var notDecodedKeys: NotDecodedKeys
let originalNotDecodedKeys: [String: [CodingKey]]

let tomlValue: TOMLValue
init(
Expand All @@ -28,6 +29,7 @@ final class InternalTOMLDecoder: Decoder {
self.dataDecoder = dataDecoder
self.strictDecoding = strictDecoding
self.notDecodedKeys = notDecodedKeys
self.originalNotDecodedKeys = notDecodedKeys.keys
}

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

// Assume that any previous container creation was related to a failed decoding
// attempt, and reset the not-decoded keys back to the value that the parent
// decoder passed to us.
self.notDecodedKeys.keys = self.originalNotDecodedKeys
return KeyedDecodingContainer<Key>(
KDC(
table: table,
Expand All @@ -64,6 +70,11 @@ final class InternalTOMLDecoder: Decoder {
)
)
}

// Assume that any previous container creation was related to a failed decoding
// attempt, and reset the not-decoded keys back to the value that the parent
// decoder passed to us.
self.notDecodedKeys.keys = self.originalNotDecodedKeys
return UDC(
array,
codingPath: self.codingPath,
Expand All @@ -75,7 +86,11 @@ final class InternalTOMLDecoder: Decoder {
}

func singleValueContainer() throws -> SingleValueDecodingContainer {
SVDC(
// Assume that any previous container creation was related to a failed decoding
// attempt, and reset the not-decoded keys back to the value that the parent
// decoder passed to us.
self.notDecodedKeys.keys = self.originalNotDecodedKeys
return SVDC(
self.tomlValue,
codingPath: self.codingPath,
dataDecoder: self.dataDecoder,
Expand Down
15 changes: 13 additions & 2 deletions Sources/TOMLKit/Decoder/KeyedDecodingContainerProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,24 @@ extension InternalTOMLDecoder.KDC {
codingPath: self.codingPath + key,
dataDecoder: self.dataDecoder,
strictDecoding: self.strictDecoding,
notDecodedKeys: self.notDecodedKeys
notDecodedKeys: InternalTOMLDecoder.NotDecodedKeys()
)

self.decodedKeys.append(key.stringValue)

return try T(from: decoder)
let decodedValue = try T(from: decoder)

// Only propagate not-decoded keys if the decoding was successful.
// Otherwise `Decodable` implementations that attempt multiple
// decoding strategies in succession (trying the next if the
// previous one failed), don't work.
for (key, path) in decoder.notDecodedKeys.keys {
self.notDecodedKeys.keys[key] = path
}

return decodedValue
}

}
}

Expand Down
17 changes: 14 additions & 3 deletions Sources/TOMLKit/Decoder/SingleValueDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,20 +213,31 @@ extension InternalTOMLDecoder.SVDC {
codingPath: self.codingPath,
dataDecoder: self.dataDecoder,
strictDecoding: self.strictDecoding,
notDecodedKeys: self.notDecodedKeys
notDecodedKeys: InternalTOMLDecoder.NotDecodedKeys()
)

let decodedValue: T
if type is Data.Type {
if let data = self.dataDecoder(self.tomlValue) {
return data as! T
decodedValue = data as! T
} else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: self.codingPath,
debugDescription: "Unable to decode Data from: \"\(self.tomlValue.debugDescription)\"."
))
}
} else {
return try T(from: decoder)
decodedValue = try T(from: decoder)
}

// Only propagate not-decoded keys if the decoding was successful.
// Otherwise `Decodable` implementations that attempt multiple
// decoding strategies in succession (trying the next if the
// previous one failed), don't work.
for (key, path) in decoder.notDecodedKeys.keys {
self.notDecodedKeys.keys[key] = path
}

return decodedValue
}
}
16 changes: 13 additions & 3 deletions Sources/TOMLKit/Decoder/UnkeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,21 @@ extension InternalTOMLDecoder.UDC {
codingPath: self.codingPath + TOMLCodingKey(index: self.currentIndex),
dataDecoder: self.dataDecoder,
strictDecoding: self.strictDecoding,
notDecodedKeys: self.notDecodedKeys
notDecodedKeys: InternalTOMLDecoder.NotDecodedKeys()
)
let decodable = try T(from: decoder)

let decodedValue = try T(from: decoder)
self.currentIndex += 1
return decodable

// Only propagate not-decoded keys if the decoding was successful.
// Otherwise `Decodable` implementations that attempt multiple
// decoding strategies in succession (trying the next if the
// previous one failed), don't work.
for (key, path) in decoder.notDecodedKeys.keys {
self.notDecodedKeys.keys[key] = path
}

return decodedValue
}
}

Expand Down
88 changes: 79 additions & 9 deletions Tests/TOMLKitTests/TOMLKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,6 @@ enum CodableEnum: String, Codable, Equatable {
case abc
case def
case ghi

public static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.abc, .abc): return true
case (.def, .def): return true
case (.ghi, .ghi): return true
default: return false
}
}
}

struct CodableStruct: Codable, Equatable {
Expand Down Expand Up @@ -501,4 +492,83 @@ final class TOMLKitTests: XCTestCase {
let _ = try udc.nestedUnkeyedContainer()
XCTAssertEqual(udc.currentIndex, 1)
}

// Tests for a bug where not-decoded keys would carry over from unsuccessful
// decoding attempts, breaking the common 'try one thing then retry another'
// decoding pattern. This bug was specific to decoders with
// `strictDecoding: true`.
func testRetryKeyedOrDictionary() throws {
enum StructOrDictionary: Decodable, Equatable {
case `struct`(value: Int)
case dictionary([String: Int])

enum CodingKeys: String, CodingKey {
case value
}

init(from decoder: Decoder) throws {
if let container = try? decoder.container(keyedBy: CodingKeys.self) {
if let value = try? container.decode(Int.self, forKey: .value) {
self = .struct(value: value)
}
}

if let container = try? decoder.singleValueContainer() {
self = try .dictionary(container.decode([String: Int].self))
} else {
throw DecodingError.dataCorrupted(.init(
codingPath: [],
debugDescription: "Expected int or keyedInt"
))
}
}
}

let toml = "other_value = 1"

// Before the bug got fixed, this line would throw an unused key error.
let value = try TOMLDecoder(strictDecoding: true).decode(StructOrDictionary.self, from: toml)

// We don't actually expect this to fail, it's not the point of the test, but
// might as well assert it just in case.
XCTAssertEqual(value, .dictionary(["other_value": 1]))
}

// This is related to the test above, but tests for a different aspect of the
// bug (I originally fixed one but not the other so they are kind of independent).
func testRetryKeyedOrInt() throws {
struct SimpleCodableStruct: Codable, Equatable {
var value: Int
}

enum SimpleOrComplex: Decodable, Equatable {
case simple(SimpleCodableStruct)
case complex(CodableStruct)

init(from decoder: Decoder) throws {
if let container = try? decoder.singleValueContainer() {
if let value = try? container.decode(CodableStruct.self) {
self = .complex(value)
return
} else if let value = try? container.decode(SimpleCodableStruct.self) {
self = .simple(value)
return
}
}
throw DecodingError.dataCorrupted(.init(
codingPath: [],
debugDescription: "Expected CodableStruct or SimpleCodableStruct"
))
}
}

let toml = "value = 2"

// Before the bug got fixed, this line would throw an unused key error.
let value = try TOMLDecoder(strictDecoding: true).decode(SimpleOrComplex.self, from: toml)

// We don't actually expect this to fail, it's not the point of the test, but
// might as well assert it just in case.
XCTAssertEqual(value, .simple(SimpleCodableStruct(value: 2)))
}
}

0 comments on commit a6d9211

Please sign in to comment.