diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index 4d87901e..07e1cb15 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -71,6 +71,12 @@ class XMLDecoderImplementation: Decoder { } public func container(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer { + if let keyed = try self.topContainer() as? SharedBox { + return KeyedDecodingContainer(XMLKeyedDecodingContainer( + referencing: self, + wrapping: keyed + )) + } if Key.self is XMLChoiceCodingKey.Type { return try choiceContainer(keyedBy: keyType) } else { @@ -78,9 +84,46 @@ class XMLDecoderImplementation: Decoder { } } - public func keyedContainer(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { + public func unkeyedContainer() throws -> UnkeyedDecodingContainer { let topContainer = try self.topContainer() + guard !topContainer.isNull else { + throw DecodingError.valueNotFound( + UnkeyedDecodingContainer.self, + DecodingError.Context( + codingPath: codingPath, + debugDescription: + """ + Cannot get unkeyed decoding container -- found null box instead. + """ + ) + ) + } + + switch topContainer { + case let unkeyed as SharedBox: + return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed) + case let keyed as SharedBox: + return XMLUnkeyedDecodingContainer( + referencing: self, + wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleKeyedBox.init) }) + ) + default: + throw DecodingError.typeMismatch( + at: codingPath, + expectation: [Any].self, + reality: topContainer + ) + } + } + + public func singleValueContainer() throws -> SingleValueDecodingContainer { + return self + } + + private func keyedContainer(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { + let topContainer = try self.topContainer() + let keyedBox: KeyedBox switch topContainer { case _ where topContainer.isNull: throw DecodingError.valueNotFound( @@ -94,33 +137,20 @@ class XMLDecoderImplementation: Decoder { ) ) case let string as StringBox: - return KeyedDecodingContainer(XMLKeyedDecodingContainer( - referencing: self, - wrapping: SharedBox(KeyedBox( - elements: KeyedStorage([("", string)]), - attributes: KeyedStorage() - )) - )) + keyedBox = KeyedBox( + elements: KeyedStorage([("", string)]), + attributes: KeyedStorage() + ) case let containsEmpty as SingleKeyedBox where containsEmpty.element is NullBox: - return KeyedDecodingContainer(XMLKeyedDecodingContainer( - referencing: self, wrapping: SharedBox(KeyedBox( - elements: KeyedStorage([("", StringBox(""))]), attributes: KeyedStorage() - )) - )) - case let keyed as SharedBox: - return KeyedDecodingContainer(XMLKeyedDecodingContainer( - referencing: self, - wrapping: keyed - )) + keyedBox = KeyedBox( + elements: KeyedStorage([("", StringBox(""))]), + attributes: KeyedStorage() + ) case let unkeyed as SharedBox: guard let keyed = unkeyed.withShared({ $0.first }) as? KeyedBox else { fallthrough } - - return KeyedDecodingContainer(XMLKeyedDecodingContainer( - referencing: self, - wrapping: SharedBox(keyed) - )) + keyedBox = keyed default: throw DecodingError.typeMismatch( at: codingPath, @@ -128,23 +158,23 @@ class XMLDecoderImplementation: Decoder { reality: topContainer ) } + let container = XMLKeyedDecodingContainer( + referencing: self, + wrapping: SharedBox(keyedBox) + ) + return KeyedDecodingContainer(container) } /// - Returns: A `KeyedDecodingContainer` for an XML choice element. - public func choiceContainer(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { + private func choiceContainer(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { let topContainer = try self.topContainer() - let choiceBox: ChoiceBox? + let choiceBox: ChoiceBox switch topContainer { case let choice as ChoiceBox: choiceBox = choice case let singleKeyed as SingleKeyedBox: choiceBox = ChoiceBox(singleKeyed) - case let keyed as SharedBox: - choiceBox = ChoiceBox(keyed.withShared { $0 }) default: - choiceBox = nil - } - guard let box = choiceBox else { throw DecodingError.typeMismatch( at: codingPath, expectation: [String: Any].self, @@ -153,47 +183,10 @@ class XMLDecoderImplementation: Decoder { } let container = XMLChoiceDecodingContainer( referencing: self, - wrapping: SharedBox(box) + wrapping: SharedBox(choiceBox) ) return KeyedDecodingContainer(container) } - - public func unkeyedContainer() throws -> UnkeyedDecodingContainer { - let topContainer = try self.topContainer() - - guard !topContainer.isNull else { - throw DecodingError.valueNotFound( - UnkeyedDecodingContainer.self, - DecodingError.Context( - codingPath: codingPath, - debugDescription: - """ - Cannot get unkeyed decoding container -- found null box instead. - """ - ) - ) - } - - switch topContainer { - case let unkeyed as SharedBox: - return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed) - case let keyed as SharedBox: - return XMLUnkeyedDecodingContainer( - referencing: self, - wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleKeyedBox.init) }) - ) - default: - throw DecodingError.typeMismatch( - at: codingPath, - expectation: [Any].self, - reality: topContainer - ) - } - } - - public func singleValueContainer() throws -> SingleValueDecodingContainer { - return self - } } // MARK: - Concrete Value Representations diff --git a/Tests/XMLCoderTests/MixedChoiceAndNonChoiceTests.swift b/Tests/XMLCoderTests/MixedChoiceAndNonChoiceTests.swift index e1e16638..e2eca673 100644 --- a/Tests/XMLCoderTests/MixedChoiceAndNonChoiceTests.swift +++ b/Tests/XMLCoderTests/MixedChoiceAndNonChoiceTests.swift @@ -8,12 +8,45 @@ import XCTest import XMLCoder +private enum AlternateIntOrString: Equatable { + case alternateInt(Int) + case alternateString(String) +} + +extension AlternateIntOrString: Codable { + enum CodingKeys: String, CodingKey { + case alternateInt = "alternate-int" + case alternateString = "alternate-string" + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .alternateInt(int): + try container.encode(int, forKey: .alternateInt) + case let .alternateString(string): + try container.encode(string, forKey: .alternateString) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if container.contains(.alternateInt) { + self = .alternateInt(try container.decode(Int.self, forKey: .alternateInt)) + } else { + self = .alternateString(try container.decode(String.self, forKey: .alternateString)) + } + } +} + +extension AlternateIntOrString.CodingKeys: XMLChoiceCodingKey {} + private struct MixedIntOrStringFirst: Equatable { let intOrString: IntOrString let otherValue: String } -extension MixedIntOrStringFirst: Encodable { +extension MixedIntOrStringFirst: Codable { enum CodingKeys: String, CodingKey { case otherValue = "other-value" } @@ -23,6 +56,12 @@ extension MixedIntOrStringFirst: Encodable { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(otherValue, forKey: .otherValue) } + + init(from decoder: Decoder) throws { + intOrString = try IntOrString(from: decoder) + let container = try decoder.container(keyedBy: CodingKeys.self) + otherValue = try container.decode(String.self, forKey: .otherValue) + } } private struct MixedOtherFirst: Equatable { @@ -30,7 +69,7 @@ private struct MixedOtherFirst: Equatable { let otherValue: String } -extension MixedOtherFirst: Encodable { +extension MixedOtherFirst: Codable { enum CodingKeys: String, CodingKey { case otherValue = "other-value" } @@ -40,15 +79,21 @@ extension MixedOtherFirst: Encodable { try container.encode(otherValue, forKey: .otherValue) try intOrString.encode(to: encoder) } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + otherValue = try container.decode(String.self, forKey: .otherValue) + intOrString = try IntOrString(from: decoder) + } } -private struct MixedEitherSide { +private struct MixedEitherSide: Equatable { let leading: String let intOrString: IntOrString let trailing: String } -extension MixedEitherSide: Encodable { +extension MixedEitherSide: Codable { enum CodingKeys: String, CodingKey { case leading case trailing @@ -60,18 +105,30 @@ extension MixedEitherSide: Encodable { try intOrString.encode(to: encoder) try container.encode(trailing, forKey: .trailing) } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + leading = try container.decode(String.self, forKey: .leading) + intOrString = try IntOrString(from: decoder) + trailing = try container.decode(String.self, forKey: .trailing) + } } -private struct TwoChoiceElements { +private struct TwoChoiceElements: Equatable { let first: IntOrString - let second: IntOrString + let second: AlternateIntOrString } -extension TwoChoiceElements: Encodable { +extension TwoChoiceElements: Codable { func encode(to encoder: Encoder) throws { try first.encode(to: encoder) try second.encode(to: encoder) } + + init(from decoder: Decoder) throws { + first = try IntOrString(from: decoder) + second = try AlternateIntOrString(from: decoder) + } } class MixedChoiceAndNonChoiceTests: XCTestCase { @@ -82,6 +139,14 @@ class MixedChoiceAndNonChoiceTests: XCTestCase { XCTAssertEqual(String(data: firstEncoded, encoding: .utf8), firstExpectedXML) } + func testMixedChoiceFirstDecode() throws { + let xmlString = "4other" + let xmlData = xmlString.data(using: .utf8)! + let decoded = try XMLDecoder().decode(MixedIntOrStringFirst.self, from: xmlData) + let expected = MixedIntOrStringFirst(intOrString: .int(4), otherValue: "other") + XCTAssertEqual(decoded, expected) + } + func testMixedChoiceSecondEncode() throws { let second = MixedOtherFirst(intOrString: .int(4), otherValue: "other") let secondEncoded = try XMLEncoder().encode(second, withRootKey: "container") @@ -89,6 +154,14 @@ class MixedChoiceAndNonChoiceTests: XCTestCase { XCTAssertEqual(String(data: secondEncoded, encoding: .utf8), secondExpectedXML) } + func testMixedChoiceSecondDecode() throws { + let xmlString = "other4" + let xmlData = xmlString.data(using: .utf8)! + let decoded = try XMLDecoder().decode(MixedOtherFirst.self, from: xmlData) + let expected = MixedOtherFirst(intOrString: .int(4), otherValue: "other") + XCTAssertEqual(decoded, expected) + } + func testMixedChoiceFlankedEncode() throws { let flanked = MixedEitherSide(leading: "first", intOrString: .string("then"), trailing: "second") let flankedEncoded = try XMLEncoder().encode(flanked, withRootKey: "container") @@ -98,10 +171,28 @@ class MixedChoiceAndNonChoiceTests: XCTestCase { XCTAssertEqual(String(data: flankedEncoded, encoding: .utf8), flankedExpectedXML) } + func testMixedChoiceFlankedDecode() throws { + let xmlString = """ + firstthensecond + """ + let xmlData = xmlString.data(using: .utf8)! + let decoded = try XMLDecoder().decode(MixedEitherSide.self, from: xmlData) + let expected = MixedEitherSide(leading: "first", intOrString: .string("then"), trailing: "second") + XCTAssertEqual(decoded, expected) + } + func testTwoChoiceElementsEncode() throws { - let twoChoiceElements = TwoChoiceElements(first: .int(1), second: .string("one")) + let twoChoiceElements = TwoChoiceElements(first: .int(1), second: .alternateString("one")) let encoded = try XMLEncoder().encode(twoChoiceElements, withRootKey: "container") - let expectedXML = "1one" + let expectedXML = "1one" XCTAssertEqual(String(data: encoded, encoding: .utf8), expectedXML) } + + func testTwoChoiceElementsDecode() throws { + let xmlString = "1one" + let xmlData = xmlString.data(using: .utf8)! + let decoded = try XMLDecoder().decode(TwoChoiceElements.self, from: xmlData) + let expected = TwoChoiceElements(first: .int(1), second: .alternateString("one")) + XCTAssertEqual(decoded, expected) + } } diff --git a/Tests/XMLCoderTests/XCTestManifests.swift b/Tests/XMLCoderTests/XCTestManifests.swift index 075c0cbc..e9f97926 100644 --- a/Tests/XMLCoderTests/XCTestManifests.swift +++ b/Tests/XMLCoderTests/XCTestManifests.swift @@ -419,9 +419,13 @@ extension MixedChoiceAndNonChoiceTests { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__MixedChoiceAndNonChoiceTests = [ + ("testMixedChoiceFirstDecode", testMixedChoiceFirstDecode), ("testMixedChoiceFirstEncode", testMixedChoiceFirstEncode), + ("testMixedChoiceFlankedDecode", testMixedChoiceFlankedDecode), ("testMixedChoiceFlankedEncode", testMixedChoiceFlankedEncode), + ("testMixedChoiceSecondDecode", testMixedChoiceSecondDecode), ("testMixedChoiceSecondEncode", testMixedChoiceSecondEncode), + ("testTwoChoiceElementsDecode", testTwoChoiceElementsDecode), ("testTwoChoiceElementsEncode", testTwoChoiceElementsEncode), ] }