diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index a9710354..8e50fb91 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -287,17 +287,17 @@ extension XMLKeyedDecodingContainer { switch strategy(key) { case .attribute?: - box = try getAttributeBox(attributes, key) + box = try getAttributeBox(for: type, attributes, key) case .element?: - box = elements + box = try getElementBox(for: type, elements, key) case .elementOrAttribute?: box = try getAttributeOrElementBox(attributes, elements, key) default: switch type { case is XMLAttributeProtocol.Type: - box = try getAttributeBox(attributes, key) + box = try getAttributeBox(for: type, attributes, key) case is XMLElementProtocol.Type: - box = elements + box = try getElementBox(for: type, elements, key) default: box = try getAttributeOrElementBox(attributes, elements, key) } @@ -339,9 +339,24 @@ extension XMLKeyedDecodingContainer { return unwrapped } - private func getAttributeBox(_ attributes: [KeyedBox.Attribute], _ key: Key) throws -> Box { - let attributeBox = attributes.first ?? NullBox() - return attributeBox + private func getAttributeBox(for type: T.Type, _ attributes: [KeyedBox.Attribute], _ key: Key) throws -> Box { + if let box = attributes.first { return box } + if type is AnyOptional.Type || type is XMLOptionalAttributeProtocol.Type { return NullBox() } + + throw DecodingError.keyNotFound(key, DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "No attribute found for key \(_errorDescription(of: key))." + )) + } + + private func getElementBox(for type: T.Type, _ elements: [KeyedBox.Element], _ key: Key) throws -> Box { + guard elements.isEmpty else { return elements } + if type is AnyOptional.Type || type is XMLDecodableSequence.Type { return elements } + + throw DecodingError.keyNotFound(key, DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "No element found for key \(_errorDescription(of: key))." + )) } private func getAttributeOrElementBox(_ attributes: [KeyedBox.Attribute], _ elements: [KeyedBox.Element], _ key: Key) throws -> Box { diff --git a/Tests/XMLCoderTests/AdvancedFeatures/DynamicNodeDecodingTest.swift b/Tests/XMLCoderTests/AdvancedFeatures/DynamicNodeDecodingTest.swift index afd82157..0eea0c1f 100644 --- a/Tests/XMLCoderTests/AdvancedFeatures/DynamicNodeDecodingTest.swift +++ b/Tests/XMLCoderTests/AdvancedFeatures/DynamicNodeDecodingTest.swift @@ -184,4 +184,167 @@ final class DynamicNodeDecodingTest: XCTestCase { let test = try decoder.decode(TestStruct.self, from: overlappingKeys) XCTAssertEqual(test, TestStruct(attribute: 123, element: "StringValue")) } + + struct Foo: Decodable { + var nested: T + } + + struct ArrayFoo: Decodable { + var nested: [T] + } + + struct NestedElement: Decodable, DynamicNodeDecoding { + var field1: String + var field2: String + + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { .element } + } + + struct NestedAttribute: Decodable, DynamicNodeDecoding { + var field1: String + var field2: String + + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { .attribute } + } + + struct NestedElementOrAttribute: Decodable, DynamicNodeDecoding { + var field1: String + var field2: String + + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { .elementOrAttribute } + } + + func testGenericKeyedFailsOnMissingValues() { + let failureElementXML = + """ + + value_1 + + """ + + let failureAttributeXML = + """ + + + + """ + + XCTAssertThrowsError(try XMLDecoder().decode(Foo.self, from: Data(failureElementXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertThrowsError(try XMLDecoder().decode(Foo.self, from: Data(failureElementXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertThrowsError(try XMLDecoder().decode(Foo.self, from: Data(failureElementXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + + XCTAssertThrowsError(try XMLDecoder().decode(Foo.self, from: Data(failureAttributeXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertThrowsError(try XMLDecoder().decode(Foo.self, from: Data(failureAttributeXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertThrowsError(try XMLDecoder().decode(Foo.self, from: Data(failureAttributeXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + } + + func testGenericUnkeyedFailsOnMissingValues() { + let failureAttributeXML = + """ + + + + + + """ + + let failureElementXML = + """ + + value_1 + value_1 + value_1 + + """ + + XCTAssertThrowsError(try XMLDecoder().decode(ArrayFoo.self, from: Data(failureAttributeXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertThrowsError(try XMLDecoder().decode(ArrayFoo.self, from: Data(failureAttributeXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertThrowsError(try XMLDecoder().decode(ArrayFoo.self, from: Data(failureAttributeXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + + XCTAssertThrowsError(try XMLDecoder().decode(ArrayFoo.self, from: Data(failureElementXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertThrowsError(try XMLDecoder().decode(ArrayFoo.self, from: Data(failureElementXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertThrowsError(try XMLDecoder().decode(ArrayFoo.self, from: Data(failureElementXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + } + + func testGenericKeyedSucceedsWithoutMissingValues() { + let successElementXML = + """ + + value_1value_2 + + """ + + let successAttributeXML = + """ + + + + """ + + XCTAssertNoThrow(try XMLDecoder().decode(Foo.self, from: Data(successElementXML.utf8))) + XCTAssertThrowsError(try XMLDecoder().decode(Foo.self, from: Data(successElementXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertNoThrow(try XMLDecoder().decode(Foo.self, from: Data(successElementXML.utf8))) + + XCTAssertThrowsError(try XMLDecoder().decode(Foo.self, from: Data(successAttributeXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertNoThrow(try XMLDecoder().decode(Foo.self, from: Data(successAttributeXML.utf8))) + XCTAssertNoThrow(try XMLDecoder().decode(Foo.self, from: Data(successAttributeXML.utf8))) + } + + func testGenericUnkeyedSucceedsWithoutMissingValues() { + let successElementXML = + """ + + value_1value_2 + value_1value_2 + + """ + + let successAttributeXML = + """ + + + + + """ + + XCTAssertNoThrow(try XMLDecoder().decode(ArrayFoo.self, from: Data(successElementXML.utf8))) + XCTAssertThrowsError(try XMLDecoder().decode(ArrayFoo.self, from: Data(successElementXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertNoThrow(try XMLDecoder().decode(ArrayFoo.self, from: Data(successElementXML.utf8))) + + XCTAssertThrowsError(try XMLDecoder().decode(ArrayFoo.self, from: Data(successAttributeXML.utf8))) { + guard case DecodingError.keyNotFound = $0 else { XCTFail("Invalid error thrown: \($0)"); return } + } + XCTAssertNoThrow(try XMLDecoder().decode(ArrayFoo.self, from: Data(successAttributeXML.utf8))) + XCTAssertNoThrow(try XMLDecoder().decode(ArrayFoo.self, from: Data(successAttributeXML.utf8))) + } }