diff --git a/Sources/XMLCoding/Decoder/XMLDecoder.swift b/Sources/XMLCoding/Decoder/XMLDecoder.swift index d00815f..1002286 100644 --- a/Sources/XMLCoding/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoding/Decoder/XMLDecoder.swift @@ -345,7 +345,7 @@ extension _XMLDecoder { /// Returns the given value unboxed from a container. internal func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let value = value as? String else { return nil @@ -361,7 +361,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Int.Type) throws -> Int? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -373,7 +373,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Int8.Type) throws -> Int8? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -385,7 +385,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Int16.Type) throws -> Int16? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -397,7 +397,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Int32.Type) throws -> Int32? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -409,7 +409,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Int64.Type) throws -> Int64? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -421,7 +421,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: UInt.Type) throws -> UInt? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -433,7 +433,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: UInt8.Type) throws -> UInt8? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -445,7 +445,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: UInt16.Type) throws -> UInt16? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -457,7 +457,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: UInt32.Type) throws -> UInt32? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -469,7 +469,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: UInt64.Type) throws -> UInt64? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -481,7 +481,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Float.Type) throws -> Float? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -493,7 +493,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Double.Type) throws -> Double? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { return nil } @@ -505,7 +505,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: String.Type) throws -> String? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } guard let string = value as? String else { throw DecodingError._typeMismatch(at: self.codingPath, expectation: type, reality: value) @@ -515,7 +515,9 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Date.Type) throws -> Date? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { + return nil + } switch self.options.dateDecodingStrategy { case .deferredToDate: @@ -561,7 +563,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Data.Type) throws -> Data? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } switch self.options.dataDecodingStrategy { case .deferredToData: @@ -590,7 +592,7 @@ extension _XMLDecoder { } internal func unbox(_ value: Any, as type: Decimal.Type) throws -> Decimal? { - guard !(value is NSNull) else { return nil } + guard !(value is MissingValue) else { return nil } // Attempt to bridge from NSDecimalNumber. let doubleValue = try self.unbox(value, as: Double.self)! @@ -620,7 +622,12 @@ extension _XMLDecoder { guard let decimal = try self.unbox(value, as: Decimal.self) else { return nil } decoded = decimal as! T } else { - self.storage.push(container: value) + if value is MissingValue { + self.storage.push(container: [:]) + } else { + self.storage.push(container: value) + } + decoded = try type.init(from: self) self.storage.popContainer() } diff --git a/Sources/XMLCoding/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoding/Decoder/XMLKeyedDecodingContainer.swift index 6c56901..5c70fd0 100644 --- a/Sources/XMLCoding/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoding/Decoder/XMLKeyedDecodingContainer.swift @@ -51,6 +51,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain } } + public func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: Bool.self) + } + public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -66,6 +77,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: Int.Type, forKey key: Key) throws -> Int? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: Int.self) + } + public func decode(_ type: Int.Type, forKey key: Key) throws -> Int { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -81,6 +103,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: Int8.Type, forKey key: Key) throws -> Int8? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: Int8.self) + } + public func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -96,6 +129,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: Int16.Type, forKey key: Key) throws -> Int16? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: Int16.self) + } + public func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -111,6 +155,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: Int32.Type, forKey key: Key) throws -> Int32? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: Int32.self) + } + public func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -126,6 +181,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: Int64.Type, forKey key: Key) throws -> Int64? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: Int64.self) + } + public func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -141,6 +207,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: UInt.Type, forKey key: Key) throws -> UInt? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: UInt.self) + } + public func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -156,6 +233,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: UInt8.Type, forKey key: Key) throws -> UInt8? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: UInt8.self) + } + public func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -171,6 +259,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: UInt16.Type, forKey key: Key) throws -> UInt16? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: UInt16.self) + } + public func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -186,6 +285,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: UInt32.Type, forKey key: Key) throws -> UInt32? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: UInt32.self) + } + public func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -201,6 +311,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: UInt64.Type, forKey key: Key) throws -> UInt64? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: UInt64.self) + } + public func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -216,6 +337,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: Float.Type, forKey key: Key) throws -> Float? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: Float.self) + } + public func decode(_ type: Float.Type, forKey key: Key) throws -> Float { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -231,6 +363,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: Double.Type, forKey key: Key) throws -> Double? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: Double.self) + } + public func decode(_ type: Double.Type, forKey key: Key) throws -> Double { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -246,6 +389,17 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain return value } + public func decodeIfPresent(_ type: String.Type, forKey key: Key) throws -> String? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: String.self) + } + public func decode(_ type: String.Type, forKey key: Key) throws -> String { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -255,12 +409,24 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain defer { self.decoder.codingPath.removeLast() } guard let value = try self.decoder.unbox(entry, as: String.self) else { - throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) + // treat a missing but required string as an empty string + return "" } return value } + public func decodeIfPresent(_ type: T.Type, forKey key: Key) throws -> T? { + guard let entry = self.container[key.stringValue] else { + return nil + } + + self.decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + return try self.decoder.unbox(entry, as: type) + } + public func decode(_ type: T.Type, forKey key: Key) throws -> T { guard let entry = self.container[key.stringValue] else { throw DecodingError.keyNotFound(key, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "No value associated with key \(key) (\"\(key.stringValue)\").")) @@ -270,6 +436,11 @@ internal struct _XMLKeyedDecodingContainer : KeyedDecodingContain defer { self.decoder.codingPath.removeLast() } guard let value = try self.decoder.unbox(entry, as: type) else { + if type == Data.self || type == NSData.self { + // treat a missing but required Data as an empty Data + return Data() as! T + } + throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: self.decoder.codingPath, debugDescription: "Expected \(type) value but found null instead.")) } diff --git a/Sources/XMLCoding/Encoder/XMLEncoder.swift b/Sources/XMLCoding/Encoder/XMLEncoder.swift index 6d2cd15..b3c2a8e 100644 --- a/Sources/XMLCoding/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoding/Encoder/XMLEncoder.swift @@ -907,17 +907,35 @@ extension _XMLEncoder { if let boolean = value as? Bool { return .boolean(boolean) } else if let string = value as? String { - return .string(string.description) + return .string(string) } else if let int = value as? Int64 { return .int64(int) } else if let int = value as? UInt64 { return .uint64(int) } else if let double = value as? Double { return .double(double) + } else if let double = value as? Float { + return .double(Double(double)) } else if let date = value as? Date { return try self.box(date) } else if let data = value as? Data { return try self.box(data) + } else if let int = value as? Int { + return .int64(Int64(int)) + } else if let int = value as? Int8 { + return .int64(Int64(int)) + } else if let int = value as? Int16 { + return .int64(Int64(int)) + } else if let int = value as? Int32 { + return .int64(Int64(int)) + } else if let int = value as? UInt { + return .uint64(UInt64(int)) + } else if let int = value as? UInt8 { + return .uint64(UInt64(int)) + } else if let int = value as? UInt16 { + return .uint64(UInt64(int)) + } else if let int = value as? UInt32 { + return .uint64(UInt64(int)) } let depth = self.storage.count diff --git a/Sources/XMLCoding/XMLStackParser.swift b/Sources/XMLCoding/XMLStackParser.swift index 6a9f13d..962727a 100644 --- a/Sources/XMLCoding/XMLStackParser.swift +++ b/Sources/XMLCoding/XMLStackParser.swift @@ -12,6 +12,10 @@ import Foundation // Data Representation //===----------------------------------------------------------------------===// +internal struct MissingValue { + +} + public struct XMLHeader { /// the XML standard that the produced document conforms to. var version: Double? = nil @@ -185,8 +189,8 @@ internal class _XMLElement { } // if the node is empty and there is no existing value } else if node[childElement.key] == nil { - // an empty node can be treated as an empty dictionary - node[childElement.key] = [:] + // we mark an empty node as having a missing value + node[childElement.key] = MissingValue() } } } diff --git a/Tests/XMLCodingTests/XCTestManifests.swift b/Tests/XMLCodingTests/XCTestManifests.swift index cec1bbe..7b059e4 100644 --- a/Tests/XMLCodingTests/XCTestManifests.swift +++ b/Tests/XMLCodingTests/XCTestManifests.swift @@ -2,12 +2,20 @@ import XCTest extension XMLParsingTests { static let __allTests = [ - ("testEmptyElement", testEmptyElement), - ("testEmptyElementNotEffectingPreviousElement", testEmptyElementNotEffectingPreviousElement), - ("testListDecodingWithCollapseItemTagStrategy", testListDecodingWithCollapseItemTagStrategy), + ("testValuesElement", testValuesElement), + ("testEmptyValuesElement", testEmptyValuesElement), + ("testFailOnMissingStringElement", testFailOnMissingStringElement), + ("testFailOnMissingDataElement", testFailOnMissingDataElement), + ("testOptionalEmptyValuesElement", testOptionalEmptyValuesElement), + ("testOptionalMissingValuesElement", testOptionalMissingValuesElement), + ("testOptionalEmptyValuesCombinedTagElement", testOptionalEmptyValuesCombinedTagElement), + ("testValuesWithISO8601DateElement", testValuesWithISO8601DateElement), + ("testEmptyStructureElement", testEmptyStructureElement), + ("testEmptyStructureElementNotEffectingPreviousElement", testEmptyStructureElementNotEffectingPreviousElement), ("testListDecodingWithDefaultStrategy", testListDecodingWithDefaultStrategy), - ("testSingletonListDecodingWithCollapseItemTagStrategy", testSingletonListDecodingWithCollapseItemTagStrategy), ("testSingletonListDecodingWithDefaultStrategy", testSingletonListDecodingWithDefaultStrategy), + ("testListDecodingWithCollapseItemTagStrategy", testListDecodingWithCollapseItemTagStrategy), + ("testSingletonListDecodingWithCollapseItemTagStrategy", testSingletonListDecodingWithCollapseItemTagStrategy) ] } diff --git a/Tests/XMLCodingTests/XMLParsingTests.swift b/Tests/XMLCodingTests/XMLParsingTests.swift index ce761e6..1522deb 100644 --- a/Tests/XMLCodingTests/XMLParsingTests.swift +++ b/Tests/XMLCodingTests/XMLParsingTests.swift @@ -40,9 +40,31 @@ class XMLParsingTests: XCTestCase { struct Metadata: Codable, Equatable { let id: String + let optionalString: String? + let data: Data? + let date: Date? + let bool: Bool? + let int: Int? + let double: Double? enum CodingKeys: String, CodingKey { case id = "Id" + case optionalString = "OptionalString" + case data = "Data" + case date = "Date" + case bool = "Bool" + case int = "Int" + case double = "Double" + } + } + + struct MetadataWithData: Codable, Equatable { + let id: String + let data: Data + + enum CodingKeys: String, CodingKey { + case id = "Id" + case data = "Data" } } @@ -54,7 +76,7 @@ class XMLParsingTests: XCTestCase { } } - struct Response: Codable { + struct Response: Codable, Equatable { let result: Result let metadata: Metadata @@ -64,6 +86,16 @@ class XMLParsingTests: XCTestCase { } } + struct ResponseWithData: Codable, Equatable { + let result: Result + let metadata: MetadataWithData + + enum CodingKeys: String, CodingKey { + case result = "Result" + case metadata = "Metadata" + } + } + struct ResponseWithList: Codable, Equatable { let result: Result let metadataList: MetadataList @@ -84,7 +116,294 @@ class XMLParsingTests: XCTestCase { } } - func testEmptyElement() throws { + /// Test that we can decode/encode fields of various types + func testValuesElement() throws { + let inputString = """ + + + Hello + + + id + ZGF0YTE= + 1534352914 + true + 77 + 45.345 + + + """ + + guard let inputData = inputString.data(using: .utf8) else { + return XCTFail() + } + + let response = try XMLDecoder().decode(Response.self, from: inputData) + + // encode the output to make sure we get what we started with + let encoder = XMLEncoder() + encoder.dateEncodingStrategy = .secondsSince1970 // to match decoder strategy + let data = try encoder.encode(response, withRootKey: "Response") + + let response2 = try XMLDecoder().decode(Response.self, from: data) + XCTAssertEqual(response, response2) + + XCTAssertEqual("data1", String(data: response.metadata.data!, encoding: .utf8)) // decode the data + XCTAssertEqual(Date(timeIntervalSince1970: 1534352914), response.metadata.date) // decode the date + XCTAssertEqual(77, response.metadata.int) // decode the integer + XCTAssertEqual(true, response.metadata.bool) // decode the boolean + + let double = response.metadata.double! + XCTAssertTrue(double > 45 && double < 46) + } + + /// Test that we can decode/encode required but empty string and data fields correctly + func testEmptyValuesElement() throws { + let inputString = """ + + + Hello + + + + + + + """ + + guard let inputData = inputString.data(using: .utf8) else { + return XCTFail() + } + + let response = try XMLDecoder().decode(ResponseWithData.self, from: inputData) + + // encode the output to make sure we get what we started with + let encoder = XMLEncoder() + encoder.dateEncodingStrategy = .secondsSince1970 // to match decoder strategy + let data = try encoder.encode(response, withRootKey: "Response") + + let response2 = try XMLDecoder().decode(ResponseWithData.self, from: data) + XCTAssertEqual(response, response2) + + // has a zero length string + XCTAssertEqual(0, response.metadata.id.count) + // has a zero length data + XCTAssertEqual(0, response.metadata.data.count) + } + + /// Test that we only substitute an empty string if the field is present + func testFailOnMissingStringElement() throws { + let inputString = """ + + + Hello + + + + + + """ + + guard let inputData = inputString.data(using: .utf8) else { + return XCTFail() + } + + do { + _ = try XMLDecoder().decode(ResponseWithData.self, from: inputData) + } catch { + return + } + + XCTFail("Decoding should have failed") + } + + /// Test that we only substitute an empty data if the field is present + func testFailOnMissingDataElement() throws { + let inputString = """ + + + Hello + + + + + + """ + + guard let inputData = inputString.data(using: .utf8) else { + return XCTFail() + } + + do { + _ = try XMLDecoder().decode(ResponseWithData.self, from: inputData) + } catch { + return + } + + XCTFail("Decoding should have failed") + } + + /// Test that we can decode/encode empty optional fields correctly as nil optionals + func testOptionalEmptyValuesElement() throws { + let inputString = """ + + + Hello + + + id + + + + + + + + + """ + + guard let inputData = inputString.data(using: .utf8) else { + return XCTFail() + } + + let response = try XMLDecoder().decode(Response.self, from: inputData) + + // encode the output to make sure we get what we started with + let encoder = XMLEncoder() + encoder.dateEncodingStrategy = .secondsSince1970 // to match decoder strategy + let data = try encoder.encode(response, withRootKey: "Response") + + let response2 = try XMLDecoder().decode(Response.self, from: data) + XCTAssertEqual(response, response2) + + XCTAssertNil(response.metadata.optionalString) // the OptionalString tag is empty and optional + XCTAssertNil(response.metadata.data) // the Data tag is empty and optional + XCTAssertNil(response.metadata.date) // the Date tag is empty and optional + XCTAssertNil(response.metadata.bool) // the Bool tag is empty and optional + XCTAssertNil(response.metadata.int) // the Int tag is empty and optional + XCTAssertNil(response.metadata.double) // the Double tag is empty and optional + } + + /// Test that we can decode/encode missing optional fields correctly as nil optionals + func testOptionalMissingValuesElement() throws { + let inputString = """ + + + Hello + + + id + + + """ + + guard let inputData = inputString.data(using: .utf8) else { + return XCTFail() + } + + let response = try XMLDecoder().decode(Response.self, from: inputData) + + // encode the output to make sure we get what we started with + let encoder = XMLEncoder() + encoder.dateEncodingStrategy = .secondsSince1970 // to match decoder strategy + let data = try encoder.encode(response, withRootKey: "Response") + + let response2 = try XMLDecoder().decode(Response.self, from: data) + XCTAssertEqual(response, response2) + + XCTAssertNil(response.metadata.optionalString) // the OptionalString tag is empty and optional + XCTAssertNil(response.metadata.data) // the Data tag is empty and optional + XCTAssertNil(response.metadata.date) // the Date tag is empty and optional + XCTAssertNil(response.metadata.bool) // the Bool tag is empty and optional + XCTAssertNil(response.metadata.double) // the Double tag is empty and optional + } + + /// Test that we can decode/encode empty single-tag optional fields correctly as nil optionals + func testOptionalEmptyValuesCombinedTagElement() throws { + let inputString = """ + + + Hello + + + id + + + + + + + + + """ + + guard let inputData = inputString.data(using: .utf8) else { + return XCTFail() + } + + let response = try XMLDecoder().decode(Response.self, from: inputData) + + // encode the output to make sure we get what we started with + let encoder = XMLEncoder() + encoder.dateEncodingStrategy = .secondsSince1970 // to match decoder strategy + let data = try encoder.encode(response, withRootKey: "Response") + + let response2 = try XMLDecoder().decode(Response.self, from: data) + XCTAssertEqual(response, response2) + + XCTAssertNil(response.metadata.optionalString) // the OptionalString tag is empty and optional + XCTAssertNil(response.metadata.data) // the Data tag is empty and optional + XCTAssertNil(response.metadata.date) // the Date tag is empty and optional + XCTAssertNil(response.metadata.bool) // the Bool tag is empty and optional + XCTAssertNil(response.metadata.int) // the Int tag is empty and optional + XCTAssertNil(response.metadata.double) // the Double tag is empty and optional + } + + /// Test that we can decode/encode ISO8601 dates correctly + func testValuesWithISO8601DateElement() throws { + if #available(OSX 10.12, *) { + let inputString = """ + + + Hello + + + id + ZGF0YTE= + 2018-08-15T10:08:34-07:00 + true + 45.345 + + + """ + + guard let inputData = inputString.data(using: .utf8) else { + return XCTFail() + } + + let decoder = XMLDecoder() + decoder.dateDecodingStrategy = .iso8601 + let response = try decoder.decode(Response.self, from: inputData) + + // encode the output to make sure we get what we started with + let encoder = XMLEncoder() + encoder.dateEncodingStrategy = .iso8601 // to match decoder strategy + let data = try encoder.encode(response, withRootKey: "Response") + + let response2 = try decoder.decode(Response.self, from: data) + XCTAssertEqual(response, response2) + + XCTAssertEqual("data1", String(data: response.metadata.data!, encoding: .utf8)) // decode the data + XCTAssertEqual(Date(timeIntervalSince1970: 1534352914), response.metadata.date) // decode the date + XCTAssertEqual(true, response.metadata.bool) // decode the boolean + + let double = response.metadata.double! + XCTAssertTrue(double > 45 && double < 46) + } + } + + /// Test that we can decode/encode empty structures correctly + func testEmptyStructureElement() throws { let inputString = """ @@ -103,7 +422,8 @@ class XMLParsingTests: XCTestCase { XCTAssertNil(response.result.message) } - func testEmptyElementNotEffectingPreviousElement() throws { + /// Test that we can decode/encode multiple empty structures without a later one wiping out the former + func testEmptyStructureElementNotEffectingPreviousElement() throws { let inputString = """ @@ -125,6 +445,7 @@ class XMLParsingTests: XCTestCase { XCTAssertEqual("message", response.result.message) } + /// Test that we can decode/encode lists with the default strategy func testListDecodingWithDefaultStrategy() throws { guard let inputData = LIST_XML.data(using: .utf8) else { return XCTFail() @@ -135,12 +456,14 @@ class XMLParsingTests: XCTestCase { XCTAssertEqual(3, response.metadataList.items.count) // encode the output to make sure we get what we started with - let data = try XMLEncoder().encode(response, withRootKey: "Response") + let encoder = XMLEncoder() + let data = try encoder.encode(response, withRootKey: "Response") let response2 = try XMLDecoder().decode(ResponseWithList.self, from: data) XCTAssertEqual(response, response2) } + /// Test that we can decode/encode single element lists with the default strategy func testSingletonListDecodingWithDefaultStrategy() throws { guard let inputData = SINGLETON_LIST_XML.data(using: .utf8) else { return XCTFail() @@ -157,6 +480,7 @@ class XMLParsingTests: XCTestCase { XCTAssertEqual(response, response2) } + /// Test that we can decode/encode lists with the collapsing strategy func testListDecodingWithCollapseItemTagStrategy() throws { guard let inputData = LIST_XML.data(using: .utf8) else { return XCTFail() @@ -178,6 +502,7 @@ class XMLParsingTests: XCTestCase { XCTAssertEqual(response, response2) } + /// Test that we can decode/encode single element lists with the collapsing strategy func testSingletonListDecodingWithCollapseItemTagStrategy() throws { guard let inputData = SINGLETON_LIST_XML.data(using: .utf8) else { return XCTFail() @@ -200,10 +525,19 @@ class XMLParsingTests: XCTestCase { } static var allTests = [ - ("testEmptyElement", testEmptyElement), - ("testEmptyElementNotEffectingPreviousElement", testEmptyElementNotEffectingPreviousElement), + ("testValuesElement", testValuesElement), + ("testEmptyValuesElement", testEmptyValuesElement), + ("testFailOnMissingStringElement", testFailOnMissingStringElement), + ("testFailOnMissingDataElement", testFailOnMissingDataElement), + ("testOptionalEmptyValuesElement", testOptionalEmptyValuesElement), + ("testOptionalMissingValuesElement", testOptionalMissingValuesElement), + ("testOptionalEmptyValuesCombinedTagElement", testOptionalEmptyValuesCombinedTagElement), + ("testValuesWithISO8601DateElement", testValuesWithISO8601DateElement), + ("testEmptyStructureElement", testEmptyStructureElement), + ("testEmptyStructureElementNotEffectingPreviousElement", testEmptyStructureElementNotEffectingPreviousElement), ("testListDecodingWithDefaultStrategy", testListDecodingWithDefaultStrategy), ("testSingletonListDecodingWithDefaultStrategy", testSingletonListDecodingWithDefaultStrategy), - ("testListDecodingWithCollapseItemTagStrategy", testListDecodingWithCollapseItemTagStrategy) + ("testListDecodingWithCollapseItemTagStrategy", testListDecodingWithCollapseItemTagStrategy), + ("testSingletonListDecodingWithCollapseItemTagStrategy", testSingletonListDecodingWithCollapseItemTagStrategy) ] }