diff --git a/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift b/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift index d16c3210..e18b8fd4 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift @@ -64,6 +64,9 @@ class XMLEncoderImplementation: Encoder { // MARK: - Encoder Methods public func container(keyedBy _: Key.Type) -> KeyedEncodingContainer { + guard canEncodeNewValue else { + return mergeWithExistingKeyedContainer(keyedBy: Key.self) + } if Key.self is XMLChoiceCodingKey.Type { return choiceContainer(keyedBy: Key.self) } else { @@ -71,51 +74,6 @@ class XMLEncoderImplementation: Encoder { } } - public func keyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { - // If an existing keyed container was already requested, return that one. - let topContainer: SharedBox - if canEncodeNewValue { - // We haven't yet pushed a container at this level; do so here. - topContainer = storage.pushKeyedContainer() - } else { - guard let container = storage.lastContainer as? SharedBox else { - preconditionFailure( - """ - Attempt to push new keyed encoding container when already previously encoded \ - at this path. - """ - ) - } - - topContainer = container - } - - let container = XMLKeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) - return KeyedEncodingContainer(container) - } - - public func choiceContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { - let topContainer: SharedBox - if canEncodeNewValue { - // We haven't yet pushed a container at this level; do so here. - topContainer = storage.pushChoiceContainer() - } else { - guard let container = storage.lastContainer as? SharedBox else { - preconditionFailure( - """ - Attempt to push new (single element) keyed encoding container when already \ - previously encoded at this path. - """ - ) - } - - topContainer = container - } - - let container = XMLChoiceEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) - return KeyedEncodingContainer(container) - } - public func unkeyedContainer() -> UnkeyedEncodingContainer { // If an existing unkeyed container was already requested, return that one. let topContainer: SharedBox @@ -141,6 +99,54 @@ class XMLEncoderImplementation: Encoder { public func singleValueContainer() -> SingleValueEncodingContainer { return self } + + private func keyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { + let container = XMLKeyedEncodingContainer( + referencing: self, + codingPath: codingPath, + wrapping: storage.pushKeyedContainer() + ) + return KeyedEncodingContainer(container) + } + + private func choiceContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { + let container = XMLChoiceEncodingContainer( + referencing: self, + codingPath: codingPath, + wrapping: storage.pushChoiceContainer() + ) + return KeyedEncodingContainer(container) + } + + private func mergeWithExistingKeyedContainer(keyedBy _: Key.Type) -> KeyedEncodingContainer { + switch storage.lastContainer { + case let keyed as SharedBox: + let container = XMLKeyedEncodingContainer( + referencing: self, + codingPath: codingPath, + wrapping: keyed + ) + return KeyedEncodingContainer(container) + case let choice as SharedBox: + _ = storage.popContainer() + let keyed = KeyedBox( + elements: KeyedBox.Elements([choice.withShared { ($0.key, $0.element) }]), + attributes: [] + ) + let container = XMLKeyedEncodingContainer( + referencing: self, + codingPath: codingPath, + wrapping: storage.pushKeyedContainer(keyed) + ) + return KeyedEncodingContainer(container) + default: + preconditionFailure( + """ + No existing keyed encoding container to merge with. + """ + ) + } + } } extension XMLEncoderImplementation { diff --git a/Sources/XMLCoder/Encoder/XMLEncodingStorage.swift b/Sources/XMLCoder/Encoder/XMLEncodingStorage.swift index eecba789..669accf8 100644 --- a/Sources/XMLCoder/Encoder/XMLEncodingStorage.swift +++ b/Sources/XMLCoder/Encoder/XMLEncodingStorage.swift @@ -31,8 +31,8 @@ struct XMLEncodingStorage { return containers.last } - mutating func pushKeyedContainer() -> SharedBox { - let container = SharedBox(KeyedBox()) + mutating func pushKeyedContainer(_ keyedBox: KeyedBox = KeyedBox()) -> SharedBox { + let container = SharedBox(keyedBox) containers.append(container) return container } diff --git a/Tests/XMLCoderTests/IntOrString.swift b/Tests/XMLCoderTests/IntOrString.swift new file mode 100644 index 00000000..2fba2adb --- /dev/null +++ b/Tests/XMLCoderTests/IntOrString.swift @@ -0,0 +1,41 @@ +// +// IntOrString.swift +// XMLCoderTests +// +// Created by Benjamin Wetherfield on 11/24/19. +// + +import XMLCoder + +internal enum IntOrString: Equatable { + case int(Int) + case string(String) +} + +extension IntOrString: Codable { + enum CodingKeys: String, CodingKey { + case int + case string + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .int(value): + try container.encode(value, forKey: .int) + case let .string(value): + try container.encode(value, forKey: .string) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + do { + self = .int(try container.decode(Int.self, forKey: .int)) + } catch { + self = .string(try container.decode(String.self, forKey: .string)) + } + } +} + +extension IntOrString.CodingKeys: XMLChoiceCodingKey {} diff --git a/Tests/XMLCoderTests/MixedChoiceAndNonChoiceTests.swift b/Tests/XMLCoderTests/MixedChoiceAndNonChoiceTests.swift new file mode 100644 index 00000000..e1e16638 --- /dev/null +++ b/Tests/XMLCoderTests/MixedChoiceAndNonChoiceTests.swift @@ -0,0 +1,107 @@ +// +// MixedChoiceAndNonChoiceTests.swift +// XMLCoderTests +// +// Created by Benjamin Wetherfield on 11/24/19. +// + +import XCTest +import XMLCoder + +private struct MixedIntOrStringFirst: Equatable { + let intOrString: IntOrString + let otherValue: String +} + +extension MixedIntOrStringFirst: Encodable { + enum CodingKeys: String, CodingKey { + case otherValue = "other-value" + } + + func encode(to encoder: Encoder) throws { + try intOrString.encode(to: encoder) + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(otherValue, forKey: .otherValue) + } +} + +private struct MixedOtherFirst: Equatable { + let intOrString: IntOrString + let otherValue: String +} + +extension MixedOtherFirst: Encodable { + enum CodingKeys: String, CodingKey { + case otherValue = "other-value" + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(otherValue, forKey: .otherValue) + try intOrString.encode(to: encoder) + } +} + +private struct MixedEitherSide { + let leading: String + let intOrString: IntOrString + let trailing: String +} + +extension MixedEitherSide: Encodable { + enum CodingKeys: String, CodingKey { + case leading + case trailing + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(leading, forKey: .leading) + try intOrString.encode(to: encoder) + try container.encode(trailing, forKey: .trailing) + } +} + +private struct TwoChoiceElements { + let first: IntOrString + let second: IntOrString +} + +extension TwoChoiceElements: Encodable { + func encode(to encoder: Encoder) throws { + try first.encode(to: encoder) + try second.encode(to: encoder) + } +} + +class MixedChoiceAndNonChoiceTests: XCTestCase { + func testMixedChoiceFirstEncode() throws { + let first = MixedIntOrStringFirst(intOrString: .int(4), otherValue: "other") + let firstEncoded = try XMLEncoder().encode(first, withRootKey: "container") + let firstExpectedXML = "4other" + XCTAssertEqual(String(data: firstEncoded, encoding: .utf8), firstExpectedXML) + } + + func testMixedChoiceSecondEncode() throws { + let second = MixedOtherFirst(intOrString: .int(4), otherValue: "other") + let secondEncoded = try XMLEncoder().encode(second, withRootKey: "container") + let secondExpectedXML = "other4" + XCTAssertEqual(String(data: secondEncoded, encoding: .utf8), secondExpectedXML) + } + + func testMixedChoiceFlankedEncode() throws { + let flanked = MixedEitherSide(leading: "first", intOrString: .string("then"), trailing: "second") + let flankedEncoded = try XMLEncoder().encode(flanked, withRootKey: "container") + let flankedExpectedXML = """ + firstthensecond + """ + XCTAssertEqual(String(data: flankedEncoded, encoding: .utf8), flankedExpectedXML) + } + + func testTwoChoiceElementsEncode() throws { + let twoChoiceElements = TwoChoiceElements(first: .int(1), second: .string("one")) + let encoded = try XMLEncoder().encode(twoChoiceElements, withRootKey: "container") + let expectedXML = "1one" + XCTAssertEqual(String(data: encoded, encoding: .utf8), expectedXML) + } +} diff --git a/Tests/XMLCoderTests/SimpleChoiceTests.swift b/Tests/XMLCoderTests/SimpleChoiceTests.swift index e22ce864..86834caa 100644 --- a/Tests/XMLCoderTests/SimpleChoiceTests.swift +++ b/Tests/XMLCoderTests/SimpleChoiceTests.swift @@ -8,37 +8,6 @@ import XCTest import XMLCoder -private enum IntOrString: Equatable { - case int(Int) - case string(String) -} - -extension IntOrString: Codable { - enum CodingKeys: String, XMLChoiceCodingKey { - case int - case string - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - switch self { - case let .int(value): - try container.encode(value, forKey: .int) - case let .string(value): - try container.encode(value, forKey: .string) - } - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - do { - self = .int(try container.decode(Int.self, forKey: .int)) - } catch { - self = .string(try container.decode(String.self, forKey: .string)) - } - } -} - class SimpleChoiceTests: XCTestCase { func testIntOrStringIntDecoding() throws { let xml = """ diff --git a/Tests/XMLCoderTests/XCTestManifests.swift b/Tests/XMLCoderTests/XCTestManifests.swift index 26fd2943..075c0cbc 100644 --- a/Tests/XMLCoderTests/XCTestManifests.swift +++ b/Tests/XMLCoderTests/XCTestManifests.swift @@ -414,6 +414,18 @@ extension KeyedTests { ] } +extension MixedChoiceAndNonChoiceTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__MixedChoiceAndNonChoiceTests = [ + ("testMixedChoiceFirstEncode", testMixedChoiceFirstEncode), + ("testMixedChoiceFlankedEncode", testMixedChoiceFlankedEncode), + ("testMixedChoiceSecondEncode", testMixedChoiceSecondEncode), + ("testTwoChoiceElementsEncode", testTwoChoiceElementsEncode), + ] +} + extension MixedContainerTest { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -840,6 +852,7 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(KeyedBoxTests.__allTests__KeyedBoxTests), testCase(KeyedIntTests.__allTests__KeyedIntTests), testCase(KeyedTests.__allTests__KeyedTests), + testCase(MixedChoiceAndNonChoiceTests.__allTests__MixedChoiceAndNonChoiceTests), testCase(MixedContainerTest.__allTests__MixedContainerTest), testCase(NameSpaceTest.__allTests__NameSpaceTest), testCase(NestedAttributeChoiceTests.__allTests__NestedAttributeChoiceTests), diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 322538b5..82099ba9 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -22,9 +22,10 @@ /* 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 */; }; + B54555BC2343F5C1000D4128 /* EmptyArrayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */; }; + B5E67533238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E67532238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift */; }; + B5E67535238B4960006C8548 /* IntOrString.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E67534238B4960006C8548 /* IntOrString.swift */; }; B5EA3BB6230F237800D8D69B /* NestedChoiceArrayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */; }; B5F74472233F74E400BBDB15 /* RootLevelAttributeTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F74471233F74E400BBDB15 /* RootLevelAttributeTest.swift */; }; D11E094623491BCE00C24DCB /* DoubleBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11E094523491BCE00C24DCB /* DoubleBox.swift */; }; @@ -162,9 +163,10 @@ /* 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 = ""; }; + B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyArrayTest.swift; sourceTree = ""; }; + B5E67532238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MixedChoiceAndNonChoiceTests.swift; sourceTree = ""; }; + B5E67534238B4960006C8548 /* IntOrString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntOrString.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 = ""; }; D11E094523491BCE00C24DCB /* DoubleBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleBox.swift; sourceTree = ""; }; @@ -455,7 +457,6 @@ OBJ_113 /* MixedContainerTest.swift */, OBJ_114 /* NamespaceTest.swift */, OBJ_115 /* NestedAttributeChoiceTests.swift */, - B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */, OBJ_116 /* NestedChoiceTests.swift */, OBJ_117 /* NestingTests.swift */, OBJ_118 /* NodeEncodingStrategyTests.swift */, @@ -473,6 +474,8 @@ OBJ_127 /* SpacePreserveTest.swift */, B5EA3BB4230F235C00D8D69B /* NestedChoiceArrayTest.swift */, B54555BB2343F5C1000D4128 /* EmptyArrayTest.swift */, + B5E67532238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift */, + B5E67534238B4960006C8548 /* IntOrString.swift */, ); name = XMLCoderTests; path = Tests/XMLCoderTests; @@ -723,6 +726,7 @@ OBJ_227 /* SharedBoxTests.swift in Sources */, OBJ_228 /* StringBoxTests.swift in Sources */, OBJ_229 /* UIntBoxTests.swift in Sources */, + B5E67533238B47E5006C8548 /* MixedChoiceAndNonChoiceTests.swift in Sources */, OBJ_230 /* URLBoxTests.swift in Sources */, OBJ_231 /* UnkeyedBoxTests.swift in Sources */, OBJ_232 /* BreakfastTest.swift in Sources */, @@ -765,6 +769,7 @@ OBJ_266 /* PlantCatalog.swift in Sources */, OBJ_267 /* PlantTest.swift in Sources */, OBJ_268 /* RJISample.swift in Sources */, + B5E67535238B4960006C8548 /* IntOrString.swift in Sources */, OBJ_269 /* RJITest.swift in Sources */, OBJ_270 /* RelationshipsTest.swift in Sources */, OBJ_271 /* SimpleChoiceTests.swift in Sources */,