diff --git a/Sources/XMLCoder/Auxiliaries/Metatypes.swift b/Sources/XMLCoder/Auxiliaries/Metatypes.swift new file mode 100644 index 00000000..69afea9f --- /dev/null +++ b/Sources/XMLCoder/Auxiliaries/Metatypes.swift @@ -0,0 +1,36 @@ +// +// Metatypes.swift +// XMLCoder +// +// Created by Max Desiatov on 30/12/2018. +// + +/// Type-erased protocol helper for a metatype check in generic `decode` +/// overload. +protocol AnyEmptySequence { + init() +} + +protocol AnyArray { + static var elementType: Any.Type { get } +} + +extension Array: AnyEmptySequence, AnyArray { + static var elementType: Any.Type { + return Element.self + } +} + +extension Dictionary: AnyEmptySequence {} + +/// Type-erased protocol helper for a metatype check in generic `decode` +/// overload. +protocol AnyOptional { + init() +} + +extension Optional: AnyOptional { + init() { + self = nil + } +} diff --git a/Sources/XMLCoder/Auxiliaries/XMLElement.swift b/Sources/XMLCoder/Auxiliaries/XMLElement.swift index 86de9391..fec84ec2 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLElement.swift @@ -96,12 +96,11 @@ struct _XMLElement { if let content = child.value { switch elements[key] { case let unkeyedBox as UnkeyedBox: - var boxes = unkeyedBox.unbox() - boxes.append(StringBox(content)) - elements[key] = UnkeyedBox(boxes) + unkeyedBox.append(StringBox(content)) + elements[key] = unkeyedBox case let keyedBox as StringBox: elements[key] = UnkeyedBox([keyedBox, StringBox(content)]) - case _: + default: elements[key] = StringBox(content) } } else if !child.elements.isEmpty || !child.attributes.isEmpty { @@ -117,6 +116,16 @@ struct _XMLElement { } else { elements[key] = content } + } else { + switch elements[key] { + case let unkeyedBox as UnkeyedBox: + unkeyedBox.append(NullBox()) + elements[key] = unkeyedBox + case let keyedBox as StringBox: + elements[key] = UnkeyedBox([keyedBox, NullBox()]) + default: + elements[key] = NullBox() + } } } } diff --git a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift index 30e9fe51..4ccd7ba2 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift @@ -14,7 +14,8 @@ class _XMLStackParser: NSObject { var root: _XMLElement? private var stack: [_XMLElement] = [] - static func parse(with data: Data, errorContextLength length: UInt) throws -> KeyedBox { + static func parse(with data: Data, + errorContextLength length: UInt) throws -> KeyedBox { let parser = _XMLStackParser() let node = try parser.parse(with: data, errorContextLength: length) @@ -22,7 +23,8 @@ class _XMLStackParser: NSObject { return node.flatten() } - func parse(with data: Data, errorContextLength: UInt) throws -> _XMLElement { + func parse(with data: Data, + errorContextLength: UInt) throws -> _XMLElement { let xmlParser = XMLParser(data: data) xmlParser.delegate = self @@ -79,12 +81,19 @@ extension _XMLStackParser: XMLParserDelegate { stack = [] } - func parser(_: XMLParser, didStartElement elementName: String, namespaceURI _: String?, qualifiedName _: String?, attributes attributeDict: [String: String] = [:]) { + func parser(_: XMLParser, + didStartElement elementName: String, + namespaceURI _: String?, + qualifiedName _: String?, + attributes attributeDict: [String: String] = [:]) { let element = _XMLElement(key: elementName, attributes: attributeDict) stack.append(element) } - func parser(_: XMLParser, didEndElement _: String, namespaceURI _: String?, qualifiedName _: String?) { + func parser(_: XMLParser, + didEndElement _: String, + namespaceURI _: String?, + qualifiedName _: String?) { guard var element = stack.popLast() else { return } diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index 0482efc9..c6e83262 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -606,6 +606,7 @@ extension _XMLDecoder { func unbox(_ box: Box) throws -> T { let decoded: T let type = T.self + if type == Date.self || type == NSDate.self { let date: Date = try unbox(box) decoded = date as! T @@ -620,9 +621,10 @@ extension _XMLDecoder { decoded = decimal as! T } else { storage.push(container: box) - defer { storage.popContainer() } - return try type.init(from: self) + decoded = try type.init(from: self) + storage.popContainer() } + return decoded } } diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index 41794a5c..73f97cb7 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -8,15 +8,6 @@ import Foundation -/// Type-erased protocol helper for a metatype check in generic `decode` -/// overload. -private protocol AnyEmptySequence { - init() -} - -extension Array: AnyEmptySequence {} -extension Dictionary: AnyEmptySequence {} - // MARK: Decoding Containers struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { @@ -43,7 +34,8 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol self.container = container case .convertFromSnakeCase: // Convert the snake case keys in the container to camel case. - // If we hit a duplicate key after conversion, then we'll use the first one we saw. Effectively an undefined behavior with dictionaries. + // If we hit a duplicate key after conversion, then we'll use the + // first one we saw. Effectively an undefined behavior with dictionaries. let attributes = container.attributes.map { key, value in (XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase(key), value) } @@ -61,10 +53,14 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol self.container = KeyedBox(elements: elements, attributes: attributes) case let .custom(converter): let attributes = container.attributes.map { key, value in - (converter(decoder.codingPath + [_XMLKey(stringValue: key, intValue: nil)]).stringValue, value) + (converter(decoder.codingPath + + [_XMLKey(stringValue: key, intValue: nil)]).stringValue, + value) } let elements = container.elements.map { key, value in - (converter(decoder.codingPath + [_XMLKey(stringValue: key, intValue: nil)]).stringValue, value) + (converter(decoder.codingPath + + [_XMLKey(stringValue: key, intValue: nil)]).stringValue, + value) } self.container = KeyedBox(elements: elements, attributes: attributes) } @@ -113,15 +109,11 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol } public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { - return try decode(type, forKey: key) { decoder, box in - try decoder.unbox(box) - } + return try decodeConcrete(type, forKey: key) } public func decode(_ type: Decimal.Type, forKey key: Key) throws -> Decimal { - return try decode(type, forKey: key) { decoder, box in - try decoder.unbox(box) - } + return try decodeConcrete(type, forKey: key) } public func decode(_ type: Int.Type, forKey key: Key) throws -> Int { @@ -173,58 +165,50 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol } public func decode(_ type: String.Type, forKey key: Key) throws -> String { - return try decode(type, forKey: key) { decoder, box in - try decoder.unbox(box) - } + return try decodeConcrete(type, forKey: key) } public func decode(_ type: Date.Type, forKey key: Key) throws -> Date { - return try decode(type, forKey: key) { decoder, box in - try decoder.unbox(box) - } + return try decodeConcrete(type, forKey: key) } public func decode(_ type: Data.Type, forKey key: Key) throws -> Data { - return try decode(type, forKey: key) { decoder, box in - try decoder.unbox(box) - } + return try decodeConcrete(type, forKey: key) } public func decode(_ type: T.Type, forKey key: Key) throws -> T { let attributeNotFound = (container.attributes[key.stringValue] == nil) let elementNotFound = (container.elements[key.stringValue] == nil) - if type is AnyEmptySequence.Type, attributeNotFound, elementNotFound { - return (type as! AnyEmptySequence.Type).init() as! T + if let type = type as? AnyEmptySequence.Type, + attributeNotFound, elementNotFound { + return type.init() as! T } - return try decode(type, forKey: key) { decoder, box in - try decoder.unbox(box) - } + return try decodeConcrete(type, forKey: key) } - private func decodeSignedInteger(_ type: T.Type, forKey key: Key) throws -> T { - return try decode(type, forKey: key) { decoder, box in - try decoder.unbox(box) - } + private func decodeSignedInteger(_ type: T.Type, + forKey key: Key) throws -> T + where T: BinaryInteger & SignedInteger & Decodable { + return try decodeConcrete(type, forKey: key) } - private func decodeUnsignedInteger(_ type: T.Type, forKey key: Key) throws -> T { - return try decode(type, forKey: key) { decoder, box in - try decoder.unbox(box) - } + private func decodeUnsignedInteger(_ type: T.Type, + forKey key: Key) throws -> T + where T: BinaryInteger & UnsignedInteger & Decodable { + return try decodeConcrete(type, forKey: key) } - private func decodeFloatingPoint(_ type: T.Type, forKey key: Key) throws -> T { - return try decode(type, forKey: key) { decoder, box in - try decoder.unbox(box) - } + private func decodeFloatingPoint(_ type: T.Type, + forKey key: Key) throws -> T + where T: BinaryFloatingPoint & Decodable { + return try decodeConcrete(type, forKey: key) } - private func decode( + private func decodeConcrete( _ type: T.Type, - forKey key: Key, - decode _: (_XMLDecoder, Box) throws -> T? + forKey key: Key ) throws -> T { guard let entry = container.elements[key.stringValue] ?? container.attributes[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context( @@ -236,14 +220,22 @@ struct _XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol decoder.codingPath.append(key) defer { decoder.codingPath.removeLast() } - guard let value: T = try decoder.unbox(entry) else { + let value: T? = try decoder.unbox(entry) + + if value == nil, + let type = type as? AnyArray.Type, + type.elementType is AnyOptional.Type { + return [nil] as! T + } + + guard let unwrapped = value else { throw DecodingError.valueNotFound(type, DecodingError.Context( codingPath: decoder.codingPath, debugDescription: "Expected \(type) value but found null instead." )) } - return value + return unwrapped } public func nestedContainer(keyedBy _: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer { diff --git a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift index 80382b42..26154701 100644 --- a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift @@ -125,19 +125,22 @@ struct _XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { } } - private mutating func decodeSignedInteger(_ type: T.Type) throws -> T { + private mutating func decodeSignedInteger(_ type: T.Type) throws -> T + where T: BinaryInteger & SignedInteger & Decodable { return try decode(type) { decoder, box in try decoder.unbox(box) } } - private mutating func decodeUnsignedInteger(_ type: T.Type) throws -> T { + private mutating func decodeUnsignedInteger(_ type: T.Type) throws -> T + where T: BinaryInteger & UnsignedInteger & Decodable { return try decode(type) { decoder, box in try decoder.unbox(box) } } - private mutating func decodeFloatingPoint(_ type: T.Type) throws -> T { + private mutating func decodeFloatingPoint(_ type: T.Type) throws -> T + where T: BinaryFloatingPoint & Decodable { return try decode(type) { decoder, box in try decoder.unbox(box) } @@ -159,6 +162,13 @@ struct _XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { let box = container[self.currentIndex] let value = try decode(decoder, box) + + defer { currentIndex += 1 } + + if value == nil, let type = type as? AnyOptional.Type { + return type.init() as! T + } + guard let decoded: T = value else { throw DecodingError.valueNotFound(type, DecodingError.Context( codingPath: decoder.codingPath + [_XMLKey(index: self.currentIndex)], @@ -166,19 +176,22 @@ struct _XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { )) } - currentIndex += 1 return decoded } - public mutating func nestedContainer(keyedBy _: NestedKey.Type) throws -> KeyedDecodingContainer { + public mutating func nestedContainer( + keyedBy _: NestedKey.Type + ) throws -> KeyedDecodingContainer { decoder.codingPath.append(_XMLKey(index: currentIndex)) defer { self.decoder.codingPath.removeLast() } guard !isAtEnd else { - throw DecodingError.valueNotFound(KeyedDecodingContainer.self, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Cannot get nested keyed container -- unkeyed container is at end." - )) + throw DecodingError.valueNotFound( + KeyedDecodingContainer.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get nested keyed container -- unkeyed container is at end." + ) + ) } let value = self.container[self.currentIndex] @@ -190,11 +203,16 @@ struct _XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { } guard let keyed = value as? KeyedBox else { - throw DecodingError._typeMismatch(at: codingPath, expectation: [String: Any].self, reality: value) + throw DecodingError._typeMismatch(at: codingPath, + expectation: [String: Any].self, + reality: value) } currentIndex += 1 - let container = _XMLKeyedDecodingContainer(referencing: decoder, wrapping: keyed) + let container = _XMLKeyedDecodingContainer( + referencing: decoder, + wrapping: keyed + ) return KeyedDecodingContainer(container) } @@ -203,10 +221,12 @@ struct _XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { defer { self.decoder.codingPath.removeLast() } guard !isAtEnd else { - throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Cannot get nested keyed container -- unkeyed container is at end." - )) + throw DecodingError.valueNotFound( + UnkeyedDecodingContainer.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get nested keyed container -- unkeyed container is at end." + ) + ) } let value = container[self.currentIndex] @@ -218,7 +238,9 @@ struct _XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { } guard let unkeyed = value as? UnkeyedBox else { - throw DecodingError._typeMismatch(at: codingPath, expectation: UnkeyedBox.self, reality: value) + throw DecodingError._typeMismatch(at: codingPath, + expectation: UnkeyedBox.self, + reality: value) } currentIndex += 1 @@ -238,6 +260,8 @@ struct _XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { let value = container[self.currentIndex] currentIndex += 1 - return _XMLDecoder(referencing: value, at: decoder.codingPath, options: decoder.options) + return _XMLDecoder(referencing: value, + at: decoder.codingPath, + options: decoder.options) } } diff --git a/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift b/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift index 71f9e302..e4972ec0 100644 --- a/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift +++ b/Tests/XMLCoderTests/Auxiliary/XMLElementTests.swift @@ -23,7 +23,6 @@ class XMLElementTests: XCTestCase { XCTAssertEqual(keyed.key, "foo") XCTAssertNil(keyed.value) - debugPrint(keyed.elements) XCTAssertEqual(keyed.elements, ["foo": []]) XCTAssertEqual(keyed.attributes, [:]) } diff --git a/Tests/XMLCoderTests/Minimal/UnkeyedTests.swift b/Tests/XMLCoderTests/Minimal/UnkeyedTests.swift index 7d746fb4..210c913c 100644 --- a/Tests/XMLCoderTests/Minimal/UnkeyedTests.swift +++ b/Tests/XMLCoderTests/Minimal/UnkeyedTests.swift @@ -13,6 +13,18 @@ class UnkeyedTests: XCTestCase { let value: [String] } + struct NilContainer: Codable, Equatable { + let value: [String]? + } + + struct NestedNilContainer: Codable, Equatable { + let value: [String?] + } + + struct NilOfNilsContainer: Codable, Equatable { + let value: [String?]? + } + func testEmpty() throws { let decoder = XMLDecoder() @@ -23,6 +35,55 @@ class UnkeyedTests: XCTestCase { XCTAssertEqual(decoded.value, []) } + func testNil() throws { + let decoder = XMLDecoder() + + let xmlString = "" + let xmlData = xmlString.data(using: .utf8)! + + let decoded = try decoder.decode(NilContainer.self, from: xmlData) + XCTAssertEqual(decoded.value, nil) + } + + func testNilNil() throws { + let decoder = XMLDecoder() + + let xmlString = "" + let xmlData = xmlString.data(using: .utf8)! + + let decoded = try decoder.decode(NilOfNilsContainer.self, + from: xmlData) + XCTAssertEqual(decoded.value, nil) + } + + func testNestedNilMultiElement() throws { + let decoder = XMLDecoder() + + let xmlData = """ + + test1 + + test2 + + """.data(using: .utf8)! + + let decoded = try decoder.decode(NestedNilContainer.self, from: xmlData) + XCTAssertEqual(decoded.value, ["test1", nil, "test2"]) + } + + func testNestedNilSingleElement() throws { + let decoder = XMLDecoder() + + let xmlData = """ + + + + """.data(using: .utf8)! + + let decoded = try decoder.decode(NestedNilContainer.self, from: xmlData) + XCTAssertEqual(decoded.value, [nil]) + } + func testSingleElement() throws { let decoder = XMLDecoder() diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index d8611d1f..5c8bb7f2 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -70,6 +70,7 @@ BF9457F621CBB6BC005ACFDE /* KeyedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457EB21CBB6BC005ACFDE /* KeyedTests.swift */; }; BF9457F721CBB6BC005ACFDE /* DataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457EC21CBB6BC005ACFDE /* DataTests.swift */; }; D1E0C85321D8E65E0042A261 /* ErrorContextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E0C85121D8E6540042A261 /* ErrorContextTest.swift */; }; + D1E0C85521D91EBF0042A261 /* Metatypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E0C85421D91EBF0042A261 /* Metatypes.swift */; }; D1FC040521C7EF8200065B43 /* RJISample.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FC040421C7EF8200065B43 /* RJISample.swift */; }; OBJ_48 /* DecodingErrorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* DecodingErrorExtension.swift */; }; OBJ_49 /* XMLDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* XMLDecoder.swift */; }; @@ -163,6 +164,7 @@ BF9457EB21CBB6BC005ACFDE /* KeyedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyedTests.swift; sourceTree = ""; }; BF9457EC21CBB6BC005ACFDE /* DataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataTests.swift; sourceTree = ""; }; D1E0C85121D8E6540042A261 /* ErrorContextTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorContextTest.swift; sourceTree = ""; }; + D1E0C85421D91EBF0042A261 /* Metatypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metatypes.swift; sourceTree = ""; }; D1FC040421C7EF8200065B43 /* RJISample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RJISample.swift; sourceTree = ""; }; OBJ_10 /* DecodingErrorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodingErrorExtension.swift; sourceTree = ""; }; OBJ_11 /* XMLDecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLDecoder.swift; sourceTree = ""; }; @@ -250,6 +252,7 @@ BF9457B421CBB4DB005ACFDE /* ISO8601DateFormatter.swift */, BF9457B521CBB4DB005ACFDE /* XMLKey.swift */, BF9457B621CBB4DB005ACFDE /* XMLElement.swift */, + D1E0C85421D91EBF0042A261 /* Metatypes.swift */, ); path = Auxiliaries; sourceTree = ""; @@ -498,6 +501,7 @@ BF9457DA21CBB5D2005ACFDE /* DataBox.swift in Sources */, BF9457AB21CBB498005ACFDE /* DecimalBox.swift in Sources */, OBJ_56 /* XMLKeyedEncodingContainer.swift in Sources */, + D1E0C85521D91EBF0042A261 /* Metatypes.swift in Sources */, OBJ_57 /* XMLReferencingEncoder.swift in Sources */, BF9457BC21CBB4DB005ACFDE /* XMLElement.swift in Sources */, BF9457AA21CBB498005ACFDE /* UnkeyedBox.swift in Sources */,