diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index 04fdb023..4d87901e 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -319,10 +319,16 @@ extension XMLDecoderImplementation { } func unbox(_ box: Box) throws -> String { - let stringBox: StringBox = try typedBox(box, for: String.self) - let string = stringBox.unboxed + do { + let stringBox: StringBox = try typedBox(box, for: String.self) + return stringBox.unboxed + } catch { + if box is NullBox { + return "" + } + } - return string + return "" } func unbox(_ box: Box) throws -> Date { diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index 3a0fb39c..7f115097 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -79,8 +79,8 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { let box = elements.first ?? attributes.first - if let singleKeyed = box as? SingleKeyedBox { - return singleKeyed.element.isNull + if box is SingleKeyedBox { + return false } return box?.isNull ?? true @@ -160,14 +160,19 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { decoder.codingPath.append(key) defer { decoder.codingPath.removeLast() } - let elements = container.withShared { keyedBox in - keyedBox.elements[key.stringValue] - } + let elements = container.unboxed.elements[key.stringValue] - return XMLUnkeyedDecodingContainer( - referencing: decoder, - wrapping: SharedBox(elements) - ) + if let containsKeyed = elements as? [KeyedBox], let keyed = containsKeyed.first { + return XMLUnkeyedDecodingContainer( + referencing: decoder, + wrapping: SharedBox(keyed.elements.map(SingleKeyedBox.init)) + ) + } else { + return XMLUnkeyedDecodingContainer( + referencing: decoder, + wrapping: SharedBox(elements) + ) + } } public func superDecoder() throws -> Decoder { diff --git a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift index 513843e1..71300449 100644 --- a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift @@ -105,11 +105,15 @@ struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { var value: T? if let singleKeyed = box as? SingleKeyedBox { do { - // Drill down to the element in the case of an nested unkeyed element - value = try decode(decoder, singleKeyed.element) + value = try decode(decoder, singleKeyed) } catch { - // Specialize for choice elements - value = try decode(decoder, ChoiceBox(key: singleKeyed.key, element: singleKeyed.element)) + do { + // Drill down to the element in the case of an nested unkeyed element + value = try decode(decoder, singleKeyed.element) + } catch { + // Specialize for choice elements + value = try decode(decoder, ChoiceBox(key: singleKeyed.key, element: singleKeyed.element)) + } } } else { value = try decode(decoder, box) diff --git a/Tests/XMLCoderTests/BorderTest.swift b/Tests/XMLCoderTests/BorderTest.swift index 5dc55ad8..472c72a7 100644 --- a/Tests/XMLCoderTests/BorderTest.swift +++ b/Tests/XMLCoderTests/BorderTest.swift @@ -24,6 +24,16 @@ struct Borders: Codable, Equatable { } } +struct LeftBorders: Codable, Equatable { + let items: [LeftBorder?] + let count: Int + + enum CodingKeys: String, CodingKey { + case items = "border" + case count + } +} + struct Border: Codable, Equatable { struct Value: Codable, Equatable { let style: String? @@ -48,10 +58,30 @@ struct Border: Codable, Equatable { } } +struct LeftBorder: Codable, Equatable { + struct Value: Codable, Equatable { + let style: String? + } + + var left: Value + var right: Value? + var top: Value? + var bottom: Value? + var diagonal: Value? + var horizontal: Value? + var vertical: Value? +} + final class BorderTest: XCTestCase { func testSingleEmpty() throws { let result = try XMLDecoder().decode(Borders.self, from: xml) XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.items[0], Border()) + } + + func testLeftBorder() throws { + let result = try XMLDecoder().decode(LeftBorders.self, from: xml) + XCTAssertEqual(result.count, 1) XCTAssertEqual(result.items[0], nil) } } diff --git a/Tests/XMLCoderTests/BreakfastTest.swift b/Tests/XMLCoderTests/BreakfastTest.swift index 5b5b5cb6..80c6fe24 100644 --- a/Tests/XMLCoderTests/BreakfastTest.swift +++ b/Tests/XMLCoderTests/BreakfastTest.swift @@ -16,7 +16,6 @@ private let xml = """ Belgian Waffles $5.95 Two of our famous Belgian Waffles with plenty of real maple syrup - Strawberry Belgian Waffles diff --git a/Tests/XMLCoderTests/EmptyArrayTest.swift b/Tests/XMLCoderTests/EmptyArrayTest.swift new file mode 100644 index 00000000..8c80d680 --- /dev/null +++ b/Tests/XMLCoderTests/EmptyArrayTest.swift @@ -0,0 +1,68 @@ +// +// EmptyArrayTest.swift +// XMLCoderTests +// +// Created by Benjamin Wetherfield on 10/1/19. +// + +import XCTest +@testable import XMLCoder + +struct Empty: Equatable, Codable {} + +struct EmptyArray: Equatable, Codable { + enum CodingKeys: String, CodingKey { case empties = "empty" } + let empties: [Empty] +} + +struct EmptyWrapper: Equatable, Codable { + let empty: Empty +} + +struct OptionalEmptyWrapper: Equatable, Codable { + let empty: Empty? +} + +private let xml = """ + + + + + +""" + +private let xmlArray = """ + + + + + +""" + +private let xmlContainsEmpty = """ + + + +""" + +class EmptyArrayTest: XCTestCase { + func testEmptyArrayDecode() throws { + let decoded = try XMLDecoder().decode([Empty].self, from: xml.data(using: .utf8)!) + XCTAssertEqual(decoded, [Empty(), Empty(), Empty()]) + } + + func testWrappedEmptyArrayDecode() throws { + let decoded = try XMLDecoder().decode(EmptyArray.self, from: xmlArray.data(using: .utf8)!) + XCTAssertEqual(decoded, EmptyArray(empties: [Empty(), Empty(), Empty()])) + } + + func testWrappedEmptyDecode() throws { + let decoded = try XMLDecoder().decode(EmptyWrapper.self, from: xmlContainsEmpty.data(using: .utf8)!) + XCTAssertEqual(decoded, EmptyWrapper(empty: Empty())) + } + + func testWrappedOptionalEmptyDecode() throws { + let decoded = try XMLDecoder().decode(OptionalEmptyWrapper.self, from: xmlContainsEmpty.data(using: .utf8)!) + XCTAssertEqual(decoded, OptionalEmptyWrapper(empty: Empty())) + } +} diff --git a/Tests/XMLCoderTests/EmptyElementEmptyStringTests.swift b/Tests/XMLCoderTests/EmptyElementEmptyStringTests.swift index 554663f1..26e71525 100644 --- a/Tests/XMLCoderTests/EmptyElementEmptyStringTests.swift +++ b/Tests/XMLCoderTests/EmptyElementEmptyStringTests.swift @@ -9,6 +9,42 @@ import XCTest import XMLCoder class EmptyElementEmptyStringTests: XCTestCase { + struct ExplicitNestingContainer: Equatable, Decodable { + let things: ContainedArray + + struct ContainedArray: Equatable, Decodable { + let thing: [Thing] + + init(_ things: [Thing]) { + thing = things + } + } + } + + struct NestingContainer: Equatable, Decodable { + let things: [Thing] + + enum CodingKeys: String, CodingKey { + case things + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + var things = [Thing]() + if var thingContainer = try? container.nestedUnkeyedContainer(forKey: .things) { + while !thingContainer.isAtEnd { + things.append(try thingContainer.decode(Thing.self)) + } + } + self.things = things + } + + init(things: [Thing]) { + self.things = things + } + } + struct Parent: Equatable, Codable { let thing: Thing } @@ -58,6 +94,23 @@ class EmptyElementEmptyStringTests: XCTestCase { XCTAssertEqual(expected, result) } + func testArrayOfSomeEmptyElementStringDecoding() throws { + let xml = """ + + + Non-Empty! + Non-Empty! + + """ + let expected = [ + Thing(attribute: nil, value: ""), + Thing(attribute: "x", value: "Non-Empty!"), + Thing(attribute: nil, value: "Non-Empty!"), + ] + let result = try XMLDecoder().decode([Thing].self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } + func testNestedEmptyElementEmptyStringDecoding() throws { let xml = """ @@ -68,4 +121,88 @@ class EmptyElementEmptyStringTests: XCTestCase { let result = try XMLDecoder().decode(Parent.self, from: xml.data(using: .utf8)!) XCTAssertEqual(expected, result) } + + func testExplicitlyNestedArrayOfEmptyElementEmptyStringDecoding() throws { + let xml = """ + + + + + + + + """ + let expected = ExplicitNestingContainer( + things: .init([ + Thing(attribute: nil, value: ""), + Thing(attribute: "x", value: ""), + Thing(attribute: nil, value: ""), + ]) + ) + let result = try XMLDecoder().decode(ExplicitNestingContainer.self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } + + func testExplicitlyNestedArrayOfSomeEmptyElementEmptyStringDecoding() throws { + let xml = """ + + + + Non-Empty! + Non-Empty! + + + """ + let expected = ExplicitNestingContainer( + things: .init([ + Thing(attribute: nil, value: ""), + Thing(attribute: "x", value: "Non-Empty!"), + Thing(attribute: nil, value: "Non-Empty!"), + ]) + ) + let result = try XMLDecoder().decode(ExplicitNestingContainer.self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } + + func testNestedArrayOfEmptyElementEmptyStringDecoding() throws { + let xml = """ + + + + + + + + """ + let expected = NestingContainer( + things: [ + Thing(attribute: nil, value: ""), + Thing(attribute: "x", value: ""), + Thing(attribute: nil, value: ""), + ] + ) + let result = try XMLDecoder().decode(NestingContainer.self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } + + func testNestedArrayOfSomeEmptyElementEmptyStringDecoding() throws { + let xml = """ + + + + Non-Empty! + Non-Empty! + + + """ + let expected = NestingContainer( + things: [ + Thing(attribute: nil, value: ""), + Thing(attribute: "x", value: "Non-Empty!"), + Thing(attribute: nil, value: "Non-Empty!"), + ] + ) + let result = try XMLDecoder().decode(NestingContainer.self, from: xml.data(using: .utf8)!) + XCTAssertEqual(expected, result) + } } diff --git a/Tests/XMLCoderTests/Minimal/NullTests.swift b/Tests/XMLCoderTests/Minimal/NullTests.swift index 8cb84308..872feeb5 100644 --- a/Tests/XMLCoderTests/Minimal/NullTests.swift +++ b/Tests/XMLCoderTests/Minimal/NullTests.swift @@ -52,4 +52,18 @@ class NullTests: XCTestCase { let encoded = try encoder.encode(decoded, withRootKey: "container") XCTAssertEqual(String(data: encoded, encoding: .utf8)!, xmlString) } + + func testNullElement() { + let decoder = XMLDecoder() + + let xmlString = + """ + + + + """ + let xmlData = xmlString.data(using: .utf8)! + + XCTAssertThrowsError(try decoder.decode(Container.self, from: xmlData)) + } } diff --git a/Tests/XMLCoderTests/Minimal/OptionalTests.swift b/Tests/XMLCoderTests/Minimal/OptionalTests.swift index d16af2c7..31120d82 100644 --- a/Tests/XMLCoderTests/Minimal/OptionalTests.swift +++ b/Tests/XMLCoderTests/Minimal/OptionalTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import XMLCoder private struct ExpectNonNil: Decodable, Equatable { - var optional: String? + var optional: String? = "" private enum CodingKeys: String, CodingKey { case optional @@ -26,7 +26,7 @@ private struct ExpectNonNil: Decodable, Equatable { } private struct ExpectOptional: Decodable, Equatable { - var optional: String? + var optional: String? = "" private enum CodingKeys: String, CodingKey { case optional @@ -43,7 +43,7 @@ private struct ExpectOptional: Decodable, Equatable { } private struct DecodeIfPresent: Decodable, Equatable { - var optional: String? + var optional: String? = "" private enum CodingKeys: String, CodingKey { case optional @@ -65,9 +65,6 @@ class OptionalTests: XCTestCase { """.data(using: .utf8)! - XCTAssertThrowsError(try decoder.decode(ExpectNonNil.self, - from: xml)) - let decoded1 = try decoder.decode(ExpectOptional.self, from: xml) XCTAssertEqual(decoded1, ExpectOptional()) let decoded2 = try decoder.decode(DecodeIfPresent.self, from: xml) diff --git a/Tests/XMLCoderTests/Minimal/UnkeyedTests.swift b/Tests/XMLCoderTests/Minimal/UnkeyedTests.swift index 7033f1ab..d4f1dab5 100644 --- a/Tests/XMLCoderTests/Minimal/UnkeyedTests.swift +++ b/Tests/XMLCoderTests/Minimal/UnkeyedTests.swift @@ -68,7 +68,7 @@ class UnkeyedTests: XCTestCase { """.data(using: .utf8)! let decoded = try decoder.decode(NestedNilContainer.self, from: xmlData) - XCTAssertEqual(decoded.value, ["test1", nil, "test2"]) + XCTAssertEqual(decoded.value, ["test1", "", "test2"]) } func testNestedNilSingleElement() throws { @@ -81,7 +81,7 @@ class UnkeyedTests: XCTestCase { """.data(using: .utf8)! let decoded = try decoder.decode(NestedNilContainer.self, from: xmlData) - XCTAssertEqual(decoded.value, [nil]) + XCTAssertEqual(decoded.value, [""]) } func testSingleElement() throws { diff --git a/Tests/XMLCoderTests/RJITest.swift b/Tests/XMLCoderTests/RJITest.swift index 15e68ecb..f86d317d 100644 --- a/Tests/XMLCoderTests/RJITest.swift +++ b/Tests/XMLCoderTests/RJITest.swift @@ -80,7 +80,7 @@ private struct Channel: Codable, Equatable { let values = try decoder.container(keyedBy: CodingKeys.self) title = try values.decode(String.self, forKey: .title) link = try values.decode(URL.self, forKey: .link) - description = try values.decodeIfPresent(String.self, forKey: .description) + description = try values.decode(String.self, forKey: .description) language = try values.decode(String.self, forKey: .language) creator = try values.decode(String.self, forKey: .creator) rights = try values.decode(String.self, forKey: .rights) @@ -108,7 +108,7 @@ private struct Item: Codable, Equatable { let guid: URL let enclosure: Enclosure? let description: String - let subject: String? + let subject: String let date: Date let author: String? @@ -128,7 +128,7 @@ private struct Item: Codable, Equatable { guid = try values.decode(URL.self, forKey: .guid) enclosure = try values.decodeIfPresent(Enclosure.self, forKey: .enclosure) description = try values.decode(String.self, forKey: .description) - subject = try values.decodeIfPresent(String.self, forKey: .subject) + subject = try values.decode(String.self, forKey: .subject) date = try values.decode(Date.self, forKey: .date) author = try values.decodeIfPresent(String.self, forKey: .author) } diff --git a/Tests/XMLCoderTests/XCTestManifests.swift b/Tests/XMLCoderTests/XCTestManifests.swift index dc012d82..26fd2943 100644 --- a/Tests/XMLCoderTests/XCTestManifests.swift +++ b/Tests/XMLCoderTests/XCTestManifests.swift @@ -97,6 +97,7 @@ extension BorderTest { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__BorderTest = [ + ("testLeftBorder", testLeftBorder), ("testSingleEmpty", testSingleEmpty), ] } @@ -253,14 +254,31 @@ extension DynamicNodeEncodingTest { ] } +extension EmptyArrayTest { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__EmptyArrayTest = [ + ("testEmptyArrayDecode", testEmptyArrayDecode), + ("testWrappedEmptyArrayDecode", testWrappedEmptyArrayDecode), + ("testWrappedEmptyDecode", testWrappedEmptyDecode), + ("testWrappedOptionalEmptyDecode", testWrappedOptionalEmptyDecode), + ] +} + extension EmptyElementEmptyStringTests { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` // to regenerate. static let __allTests__EmptyElementEmptyStringTests = [ ("testArrayOfEmptyElementStringDecoding", testArrayOfEmptyElementStringDecoding), + ("testArrayOfSomeEmptyElementStringDecoding", testArrayOfSomeEmptyElementStringDecoding), ("testEmptyElementEmptyStringDecoding", testEmptyElementEmptyStringDecoding), ("testEmptyElementEmptyStringWithAttributeDecoding", testEmptyElementEmptyStringWithAttributeDecoding), + ("testExplicitlyNestedArrayOfEmptyElementEmptyStringDecoding", testExplicitlyNestedArrayOfEmptyElementEmptyStringDecoding), + ("testExplicitlyNestedArrayOfSomeEmptyElementEmptyStringDecoding", testExplicitlyNestedArrayOfSomeEmptyElementEmptyStringDecoding), + ("testNestedArrayOfEmptyElementEmptyStringDecoding", testNestedArrayOfEmptyElementEmptyStringDecoding), + ("testNestedArrayOfSomeEmptyElementEmptyStringDecoding", testNestedArrayOfSomeEmptyElementEmptyStringDecoding), ("testNestedEmptyElementEmptyStringDecoding", testNestedEmptyElementEmptyStringDecoding), ] } @@ -511,6 +529,7 @@ extension NullTests { static let __allTests__NullTests = [ ("testAttribute", testAttribute), ("testElement", testElement), + ("testNullElement", testNullElement), ] } @@ -808,6 +827,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(DecodingContainerTests.__allTests__DecodingContainerTests), testCase(DynamicNodeDecodingTest.__allTests__DynamicNodeDecodingTest), testCase(DynamicNodeEncodingTest.__allTests__DynamicNodeEncodingTest), + testCase(EmptyArrayTest.__allTests__EmptyArrayTest), testCase(EmptyElementEmptyStringTests.__allTests__EmptyElementEmptyStringTests), testCase(EmptyTests.__allTests__EmptyTests), testCase(EnumAssociatedValueTestComposite.__allTests__EnumAssociatedValueTestComposite), diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 6ea0dfe1..322538b5 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ /* Begin PBXBuildFile section */ 07E441BA2340F14B00890F46 /* EmptyElementEmptyStringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07E441B92340F14B00890F46 /* EmptyElementEmptyStringTests.swift */; }; + B54555BC2343F5C1000D4128 /* EmptyArrayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */; }; + B5EA3BB6230F237800D8D69B /* NestedChoiceArrayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */; }; 4A062D4F2341924E009BCAC1 /* CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A062D4E2341924E009BCAC1 /* CombineTests.swift */; }; B5EA3BB6230F237800D8D69B /* NestedChoiceArrayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */; }; B5F74472233F74E400BBDB15 /* RootLevelAttributeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F74471233F74E400BBDB15 /* RootLevelAttributeTest.swift */; }; @@ -160,6 +162,8 @@ /* Begin PBXFileReference section */ 07E441B92340F14B00890F46 /* EmptyElementEmptyStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyElementEmptyStringTests.swift; sourceTree = ""; }; + B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyArrayTest.swift; sourceTree = ""; }; + B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedChoiceArrayTest.swift; sourceTree = ""; }; 4A062D4E2341924E009BCAC1 /* CombineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineTests.swift; sourceTree = ""; }; B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NestedChoiceArrayTest.swift; sourceTree = ""; }; B5F74471233F74E400BBDB15 /* RootLevelAttributeTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootLevelAttributeTest.swift; sourceTree = ""; }; @@ -467,6 +471,8 @@ OBJ_125 /* SimpleChoiceTests.swift */, OBJ_126 /* SingleChildTests.swift */, OBJ_127 /* SpacePreserveTest.swift */, + B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */, + B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */, ); name = XMLCoderTests; path = Tests/XMLCoderTests; @@ -704,6 +710,7 @@ D18FBFB82348FAE500FA4F65 /* QuoteDecodingTest.swift in Sources */, OBJ_216 /* BenchmarkTests.swift in Sources */, OBJ_217 /* BooksTest.swift in Sources */, + B54555BC2343F5C1000D4128 /* EmptyArrayTest.swift in Sources */, OBJ_218 /* BorderTest.swift in Sources */, OBJ_219 /* BoolBoxTests.swift in Sources */, OBJ_220 /* DataBoxTests.swift in Sources */,