From c42bac2ab533f0250b112c81b477b5fe8753b2ff Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 14:57:14 -0700 Subject: [PATCH 01/11] Add failing SimpleChoiceTests --- Tests/XMLCoderTests/SimpleChoiceTests.swift | 69 +++++++++++++++++++++ XMLCoder.xcodeproj/project.pbxproj | 4 ++ 2 files changed, 73 insertions(+) create mode 100644 Tests/XMLCoderTests/SimpleChoiceTests.swift diff --git a/Tests/XMLCoderTests/SimpleChoiceTests.swift b/Tests/XMLCoderTests/SimpleChoiceTests.swift new file mode 100644 index 00000000..59de7c13 --- /dev/null +++ b/Tests/XMLCoderTests/SimpleChoiceTests.swift @@ -0,0 +1,69 @@ +// +// SimpleChoiceTests.swift +// XMLCoderTests +// +// Created by James Bean on 7/15/19. +// + +import XCTest +import XMLCoder + +private enum IntOrString: Equatable { + case int(Int) + case string(String) +} + +extension IntOrString: Decodable { + + enum CodingKeys: String, CodingKey { + case int + case 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 = "42" + let result = try XMLDecoder().decode(IntOrString.self, from: xml.data(using: .utf8)!) + let expected = IntOrString.int(42) + XCTAssertEqual(result, expected) + } + + func testIntOrStringStringDecoding() throws { + let xml = "forty-two" + let result = try XMLDecoder().decode(IntOrString.self, from: xml.data(using: .utf8)!) + let expected = IntOrString.string("forty-two") + XCTAssertEqual(result, expected) + } + + func testIntOrStringArrayDecoding() throws { + let xml = """ + + 1 + two + three + 4 + 5 + + """ + let result = try XMLDecoder().decode([IntOrString].self, from: xml.data(using: .utf8)!) + let expected: [IntOrString] = [ + .int(1), + .string("two"), + .string("three"), + .int(4), + .int(5), + ] + XCTAssertEqual(result, expected) + } +} diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 6a85816e..2a1f8fbd 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 1482D59A22DD2A1700AE2D6E /* XMLChoiceCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D59922DD2A1700AE2D6E /* XMLChoiceCodable.swift */; }; 1482D59C22DD2A4400AE2D6E /* XMLChoiceDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D59B22DD2A4400AE2D6E /* XMLChoiceDecodable.swift */; }; 1482D59E22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D59D22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift */; }; + 1482D5A222DD2D9400AE2D6E /* SimpleChoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D5A122DD2D9400AE2D6E /* SimpleChoiceTests.swift */; }; A61DCCD821DF9CA200C0A19D /* ClassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61DCCD621DF8DB300C0A19D /* ClassTests.swift */; }; A61FE03921E4D60B0015D993 /* UnkeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */; }; A61FE03C21E4EAB10015D993 /* KeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */; }; @@ -149,6 +150,7 @@ 1482D59922DD2A1700AE2D6E /* XMLChoiceCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLChoiceCodable.swift; sourceTree = ""; }; 1482D59B22DD2A4400AE2D6E /* XMLChoiceDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLChoiceDecodable.swift; sourceTree = ""; }; 1482D59D22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLChoiceEncodable.swift; sourceTree = ""; }; + 1482D5A122DD2D9400AE2D6E /* SimpleChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleChoiceTests.swift; sourceTree = ""; }; A61DCCD621DF8DB300C0A19D /* ClassTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassTests.swift; sourceTree = ""; }; A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnkeyedIntTests.swift; sourceTree = ""; }; A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = ""; }; @@ -405,6 +407,7 @@ D1AC9464224E3E1F004AB49B /* AttributedEnumIntrinsicTest.swift */, B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */, BF63EF1D21CEC99A001D38C5 /* BenchmarkTests.swift */, + 1482D5A122DD2D9400AE2D6E /* SimpleChoiceTests.swift */, OBJ_28 /* BooksTest.swift */, D1B6A2C02297EF5A005B8A6E /* BorderTest.swift */, OBJ_29 /* BreakfastTest.swift */, @@ -681,6 +684,7 @@ D1EC3E62225A32F500C610E3 /* BoxTreeTests.swift in Sources */, BF63EF0A21CD7C1A001D38C5 /* URLTests.swift in Sources */, BF9457CE21CBB516005ACFDE /* StringBoxTests.swift in Sources */, + 1482D5A222DD2D9400AE2D6E /* SimpleChoiceTests.swift in Sources */, D1CFC8242226B13F00B03222 /* NamespaceTest.swift in Sources */, BF9457D021CBB516005ACFDE /* UIntBoxTests.swift in Sources */, OBJ_80 /* BooksTest.swift in Sources */, From ac66c20f7da2690db603c9dafd27f04d64890602 Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 15:09:15 -0700 Subject: [PATCH 02/11] Add failing CompositeChoiceTests --- .../XMLCoderTests/CompositeChoiceTests.swift | 90 +++++++++++++++++++ Tests/XMLCoderTests/SimpleChoiceTests.swift | 49 +++++++--- XMLCoder.xcodeproj/project.pbxproj | 4 + 3 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 Tests/XMLCoderTests/CompositeChoiceTests.swift diff --git a/Tests/XMLCoderTests/CompositeChoiceTests.swift b/Tests/XMLCoderTests/CompositeChoiceTests.swift new file mode 100644 index 00000000..c1c9c2f1 --- /dev/null +++ b/Tests/XMLCoderTests/CompositeChoiceTests.swift @@ -0,0 +1,90 @@ +// +// CompositeChoiceTests.swift +// XMLCoderTests +// +// Created by James Bean on 7/15/19. +// + +import XCTest +import XMLCoder + +private struct IntWrapper: Codable, Equatable { + let wrapped: Int +} + +private struct StringWrapper: Codable, Equatable { + let wrapped: String +} + +private enum IntOrStringWrapper: Equatable { + case int(IntWrapper) + case string(StringWrapper) +} + +extension IntOrStringWrapper: Codable { + + enum CodingKeys: String, CodingKey { + case int + case string + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + do { + self = .int(try container.decode(IntWrapper.self, forKey: .int)) + } catch { + self = .string(try container.decode(StringWrapper.self, forKey: .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) + } + } +} + +class CompositeChoiceTests: XCTestCase { + + func testIntOrStringWrapper() throws { + let xml = """ + + + A Word About Woke Times + + + """ + let result = try XMLDecoder().decode(IntOrStringWrapper.self, from: xml.data(using: .utf8)!) + let expected = IntOrStringWrapper.string(StringWrapper(wrapped: "A Word About Woke Times")) + XCTAssertEqual(result, expected) + } + + func testArrayOfIntOrStringWrappers() throws { + let xml = """ + + + A Word About Woke Times + + + 9000 + + + A Word About Woke Tomes + + + """ + let result = try XMLDecoder().decode([IntOrStringWrapper].self, from: xml.data(using: .utf8)!) + let expected: [IntOrStringWrapper] = [ + .string(StringWrapper(wrapped: "A Word About Woke Times")), + .int(IntWrapper(wrapped: 9000)), + .string(StringWrapper(wrapped: "A Word About Woke Tomes")), + ] + XCTAssertEqual(result, expected) + } + + #warning("TODO: Add encoding and round-trip tests") +} diff --git a/Tests/XMLCoderTests/SimpleChoiceTests.swift b/Tests/XMLCoderTests/SimpleChoiceTests.swift index 59de7c13..9c3ab4b7 100644 --- a/Tests/XMLCoderTests/SimpleChoiceTests.swift +++ b/Tests/XMLCoderTests/SimpleChoiceTests.swift @@ -13,13 +13,22 @@ private enum IntOrString: Equatable { case string(String) } -extension IntOrString: Decodable { - +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 { @@ -48,14 +57,14 @@ class SimpleChoiceTests: XCTestCase { func testIntOrStringArrayDecoding() throws { let xml = """ - - 1 - two - three - 4 - 5 - - """ + + 1 + two + three + 4 + 5 + + """ let result = try XMLDecoder().decode([IntOrString].self, from: xml.data(using: .utf8)!) let expected: [IntOrString] = [ .int(1), @@ -66,4 +75,24 @@ class SimpleChoiceTests: XCTestCase { ] XCTAssertEqual(result, expected) } + + func testIntOrStringRoundTrip() throws { + let original = IntOrString.int(5) + let encoded = try XMLEncoder().encode(original, withRootKey: "container") + let decoded = try XMLDecoder().decode(IntOrString.self, from: encoded) + XCTAssertEqual(original, decoded) + } + + func testIntOrStringArrayRoundTrip() throws { + let original: [IntOrString] = [ + .int(1), + .string("two"), + .string("three"), + .int(4), + .int(5), + ] + let encoded = try XMLEncoder().encode(original, withRootKey: "container") + let decoded = try XMLDecoder().decode([IntOrString].self, from: encoded) + XCTAssertEqual(original, decoded) + } } diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 2a1f8fbd..72af2344 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 1482D59C22DD2A4400AE2D6E /* XMLChoiceDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D59B22DD2A4400AE2D6E /* XMLChoiceDecodable.swift */; }; 1482D59E22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D59D22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift */; }; 1482D5A222DD2D9400AE2D6E /* SimpleChoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D5A122DD2D9400AE2D6E /* SimpleChoiceTests.swift */; }; + 1482D5A422DD2F4D00AE2D6E /* CompositeChoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D5A322DD2F4D00AE2D6E /* CompositeChoiceTests.swift */; }; A61DCCD821DF9CA200C0A19D /* ClassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61DCCD621DF8DB300C0A19D /* ClassTests.swift */; }; A61FE03921E4D60B0015D993 /* UnkeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */; }; A61FE03C21E4EAB10015D993 /* KeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */; }; @@ -151,6 +152,7 @@ 1482D59B22DD2A4400AE2D6E /* XMLChoiceDecodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLChoiceDecodable.swift; sourceTree = ""; }; 1482D59D22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLChoiceEncodable.swift; sourceTree = ""; }; 1482D5A122DD2D9400AE2D6E /* SimpleChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleChoiceTests.swift; sourceTree = ""; }; + 1482D5A322DD2F4D00AE2D6E /* CompositeChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositeChoiceTests.swift; sourceTree = ""; }; A61DCCD621DF8DB300C0A19D /* ClassTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassTests.swift; sourceTree = ""; }; A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnkeyedIntTests.swift; sourceTree = ""; }; A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = ""; }; @@ -408,6 +410,7 @@ B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */, BF63EF1D21CEC99A001D38C5 /* BenchmarkTests.swift */, 1482D5A122DD2D9400AE2D6E /* SimpleChoiceTests.swift */, + 1482D5A322DD2F4D00AE2D6E /* CompositeChoiceTests.swift */, OBJ_28 /* BooksTest.swift */, D1B6A2C02297EF5A005B8A6E /* BorderTest.swift */, OBJ_29 /* BreakfastTest.swift */, @@ -727,6 +730,7 @@ BF9457F121CBB6BC005ACFDE /* FloatTests.swift in Sources */, BF8171D021D3B1BD00901EB0 /* DecodingContainerTests.swift in Sources */, BF9457EF21CBB6BC005ACFDE /* NullTests.swift in Sources */, + 1482D5A422DD2F4D00AE2D6E /* CompositeChoiceTests.swift in Sources */, OBJ_90 /* RelationshipsTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From 19f5caac8ebabab15c2dbee1b911ccc496b4cc4b Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 15:16:26 -0700 Subject: [PATCH 03/11] Require that XMLChoice(En/De)codable be (En/De)Codable --- Sources/XMLCoder/Decoder/XMLChoiceDecodable.swift | 2 +- Sources/XMLCoder/Encoder/XMLChoiceEncodable.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/XMLCoder/Decoder/XMLChoiceDecodable.swift b/Sources/XMLCoder/Decoder/XMLChoiceDecodable.swift index e3022598..415366bf 100644 --- a/Sources/XMLCoder/Decoder/XMLChoiceDecodable.swift +++ b/Sources/XMLCoder/Decoder/XMLChoiceDecodable.swift @@ -5,4 +5,4 @@ // Created by James Bean on 7/15/19. // -public protocol XMLChoiceDecodable {} +public protocol XMLChoiceDecodable: Decodable {} diff --git a/Sources/XMLCoder/Encoder/XMLChoiceEncodable.swift b/Sources/XMLCoder/Encoder/XMLChoiceEncodable.swift index 564bdf2a..81a37b83 100644 --- a/Sources/XMLCoder/Encoder/XMLChoiceEncodable.swift +++ b/Sources/XMLCoder/Encoder/XMLChoiceEncodable.swift @@ -5,4 +5,4 @@ // Created by James Bean on 7/15/19. // -public protocol XMLChoiceEncodable {} +public protocol XMLChoiceEncodable: Encodable {} From 7d94e868b1de231c3484b45a404da0dd68dba8e9 Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 15:20:58 -0700 Subject: [PATCH 04/11] Make test subjects XMLChoiceCodable --- Tests/XMLCoderTests/CompositeChoiceTests.swift | 2 +- Tests/XMLCoderTests/SimpleChoiceTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/XMLCoderTests/CompositeChoiceTests.swift b/Tests/XMLCoderTests/CompositeChoiceTests.swift index c1c9c2f1..2c58982e 100644 --- a/Tests/XMLCoderTests/CompositeChoiceTests.swift +++ b/Tests/XMLCoderTests/CompositeChoiceTests.swift @@ -21,7 +21,7 @@ private enum IntOrStringWrapper: Equatable { case string(StringWrapper) } -extension IntOrStringWrapper: Codable { +extension IntOrStringWrapper: XMLChoiceCodable { enum CodingKeys: String, CodingKey { case int diff --git a/Tests/XMLCoderTests/SimpleChoiceTests.swift b/Tests/XMLCoderTests/SimpleChoiceTests.swift index 9c3ab4b7..1605c8d5 100644 --- a/Tests/XMLCoderTests/SimpleChoiceTests.swift +++ b/Tests/XMLCoderTests/SimpleChoiceTests.swift @@ -13,7 +13,7 @@ private enum IntOrString: Equatable { case string(String) } -extension IntOrString: Codable { +extension IntOrString: XMLChoiceCodable { enum CodingKeys: String, CodingKey { case int case string From 59f1e9061fb084af6b6341f58fe596e92e034f1b Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 17:11:36 -0700 Subject: [PATCH 05/11] Bring in EWAV special case --- Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index acc800a8..6d3a047f 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -58,6 +58,11 @@ struct XMLCoderElement: Equatable { let storage = KeyedStorage() var elements = self.elements.reduce(storage) { $0.merge(element: $1) } + // Handle enum with associated value case, in which there are no attributes _or_ elements. + if let value = value, elements.isEmpty, attributes.isEmpty { + elements.append(StringBox(value), at: key) + } + // Handle attributed unkeyed value zap // Value should be zap. Detect only when no other elements exist if elements.isEmpty, let value = value { From bf40eba7a7ecef02fb990d2d75495b89a84c7322 Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 17:16:37 -0700 Subject: [PATCH 06/11] Bring in array of EWAC special case --- .../Decoder/XMLDecoderImplementation.swift | 15 ++++++++++++--- .../Decoder/XMLUnkeyedDecodingContainer.swift | 12 +++++++++++- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index fe237eba..3d5958fb 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -138,9 +138,18 @@ class XMLDecoderImplementation: Decoder { case let unkeyed as SharedBox: return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed) case let keyed as SharedBox: - guard let firstKey = keyed.withShared({ $0.elements.keys.first }) else { fallthrough } - - return XMLUnkeyedDecodingContainer(referencing: self, wrapping: SharedBox(keyed.withShared { $0.elements[firstKey] })) + // In order to support decoding enums with associated values, transform the `keyed` box + // into an unkeyed box composed of a single key-valued `KeyedBox` element for each + // key-value pair found in the original. + return XMLUnkeyedDecodingContainer( + referencing: self, + wrapping: keyed.withShared { + SharedBox($0.elements.map { key, element in + KeyedBox(elements: KeyedStorage([(key, element)]), attributes: .init()) + } + ) + } + ) default: throw DecodingError.typeMismatch( at: codingPath, diff --git a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift index ec0059f3..83579a36 100644 --- a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift @@ -102,7 +102,17 @@ struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { unkeyedBox[self.currentIndex] } - let value = try decode(decoder, box) + var value = try decode(decoder, box) + + // In order to support decoding enums with associated values, check to see if we have + // performed an injection of single key-valued `KeyedBox` elements in + // XMLDecoderImplementation.unkeyedContainer(), and attempt to decode the single element + // contained therein. + if value == nil { + if let keyed = box as? KeyedBox, keyed.elements.count == 1 { + value = try decode(decoder, keyed.elements[keyed.elements.keys[0]]) + } + } defer { currentIndex += 1 } From 84e2b11f10c9be829e65d89e8d5ce6fe5874c02b Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 21:48:10 -0700 Subject: [PATCH 07/11] Add Decoding support for choice elements --- .../Auxiliaries/Box/SingleElementBox.swift | 25 ++++++++++++++ .../Auxiliaries/XMLCoderElement.swift | 34 +++++++++---------- .../XMLCoder/Auxiliaries/XMLStackParser.swift | 2 +- .../Decoder/XMLDecoderImplementation.swift | 26 +++++++++----- .../Decoder/XMLUnkeyedDecodingContainer.swift | 12 +------ .../XMLCoderTests/CompositeChoiceTests.swift | 2 -- .../XMLCoderTests/Minimal/BoxTreeTests.swift | 15 ++------ XMLCoder.xcodeproj/project.pbxproj | 4 +++ 8 files changed, 68 insertions(+), 52 deletions(-) create mode 100644 Sources/XMLCoder/Auxiliaries/Box/SingleElementBox.swift diff --git a/Sources/XMLCoder/Auxiliaries/Box/SingleElementBox.swift b/Sources/XMLCoder/Auxiliaries/Box/SingleElementBox.swift new file mode 100644 index 00000000..99540f44 --- /dev/null +++ b/Sources/XMLCoder/Auxiliaries/Box/SingleElementBox.swift @@ -0,0 +1,25 @@ +// +// SingleElementBox.swift +// XMLCoder +// +// Created by James Bean on 7/15/19. +// + +/// A `Box` which contains a single `key` and `element` pair. This is useful for disambiguating elements which could either represent +/// an element nested in a keyed or unkeyed container, or an choice between multiple known-typed values (implemented in Swift using +/// enums with associated values). +struct SingleElementBox: SimpleBox { + let key: String + let element: Box +} + +extension SingleElementBox: Box { + + var isNull: Bool { + return false + } + + func xmlString() -> String? { + return nil + } +} diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 6d3a047f..37ef3c92 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -51,26 +51,26 @@ struct XMLCoderElement: Equatable { elements.append(element) } - func transformToBoxTree() -> KeyedBox { - let attributes = KeyedStorage(self.attributes.map { attribute in - (key: attribute.key, value: StringBox(attribute.value) as SimpleBox) - }) - let storage = KeyedStorage() - var elements = self.elements.reduce(storage) { $0.merge(element: $1) } - - // Handle enum with associated value case, in which there are no attributes _or_ elements. - if let value = value, elements.isEmpty, attributes.isEmpty { - elements.append(StringBox(value), at: key) - } + func transformToBoxTree() -> Box { + if let value = value, self.attributes.isEmpty, self.elements.isEmpty { + return SingleElementBox(key: key, element: StringBox(value)) + } else { + let attributes = KeyedStorage(self.attributes.map { attribute in + (key: attribute.key, value: StringBox(attribute.value) as SimpleBox) + }) + let storage = KeyedStorage() + var elements = self.elements.reduce(storage) { $0.merge(element: $1) } + + // Handle attributed unkeyed value zap + // Value should be zap. Detect only when no other elements exist + if elements.isEmpty, let value = value { + elements.append(StringBox(value), at: "value") + } + let keyedBox = KeyedBox(elements: elements, attributes: attributes) - // Handle attributed unkeyed value zap - // Value should be zap. Detect only when no other elements exist - if elements.isEmpty, let value = value { - elements.append(StringBox(value), at: "value") + return keyedBox } - let keyedBox = KeyedBox(elements: elements, attributes: attributes) - return keyedBox } func toXMLString(with header: XMLHeader? = nil, diff --git a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift index 7d4aae16..e020984b 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLStackParser.swift @@ -23,7 +23,7 @@ class XMLStackParser: NSObject { errorContextLength length: UInt, shouldProcessNamespaces: Bool, trimValueWhitespaces: Bool - ) throws -> KeyedBox { + ) throws -> Box { let parser = XMLStackParser(trimValueWhitespaces: trimValueWhitespaces) let node = try parser.parse( diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index 3d5958fb..75c8d5aa 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -138,17 +138,9 @@ class XMLDecoderImplementation: Decoder { case let unkeyed as SharedBox: return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed) case let keyed as SharedBox: - // In order to support decoding enums with associated values, transform the `keyed` box - // into an unkeyed box composed of a single key-valued `KeyedBox` element for each - // key-value pair found in the original. return XMLUnkeyedDecodingContainer( referencing: self, - wrapping: keyed.withShared { - SharedBox($0.elements.map { key, element in - KeyedBox(elements: KeyedStorage([(key, element)]), attributes: .init()) - } - ) - } + wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleElementBox.init) }) ) default: throw DecodingError.typeMismatch( @@ -381,7 +373,21 @@ extension XMLDecoderImplementation { return urlBox.unboxed } + func unbox(_ box: SingleElementBox) throws -> T { + do { + return try unbox(box.element) + } catch { + return try unbox( + KeyedBox( + elements: KeyedStorage([(box.key, box.element)]), + attributes: [] + ) + ) + } + } + func unbox(_ box: Box) throws -> T { + let decoded: T? let type = T.self @@ -401,6 +407,8 @@ extension XMLDecoderImplementation { type == String.self || type == NSString.self, let value = (try unbox(box) as String) as? T { decoded = value + } else if let singleElementBox = box as? SingleElementBox { + decoded = try unbox(singleElementBox) } else { storage.push(container: box) defer { diff --git a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift index 83579a36..b2538246 100644 --- a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift @@ -99,21 +99,11 @@ struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { defer { self.decoder.codingPath.removeLast() } let box = container.withShared { unkeyedBox in - unkeyedBox[self.currentIndex] + return unkeyedBox[self.currentIndex] } var value = try decode(decoder, box) - // In order to support decoding enums with associated values, check to see if we have - // performed an injection of single key-valued `KeyedBox` elements in - // XMLDecoderImplementation.unkeyedContainer(), and attempt to decode the single element - // contained therein. - if value == nil { - if let keyed = box as? KeyedBox, keyed.elements.count == 1 { - value = try decode(decoder, keyed.elements[keyed.elements.keys[0]]) - } - } - defer { currentIndex += 1 } if value == nil, let type = type as? AnyOptional.Type, diff --git a/Tests/XMLCoderTests/CompositeChoiceTests.swift b/Tests/XMLCoderTests/CompositeChoiceTests.swift index 2c58982e..a31662d2 100644 --- a/Tests/XMLCoderTests/CompositeChoiceTests.swift +++ b/Tests/XMLCoderTests/CompositeChoiceTests.swift @@ -85,6 +85,4 @@ class CompositeChoiceTests: XCTestCase { ] XCTAssertEqual(result, expected) } - - #warning("TODO: Add encoding and round-trip tests") } diff --git a/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift b/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift index 350c15c5..a72189ac 100644 --- a/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift +++ b/Tests/XMLCoderTests/Minimal/BoxTreeTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import XMLCoder class BoxTreeTests: XCTestCase { - func testNestedValues() { + func testNestedValues() throws { let e1 = XMLCoderElement( key: "foo", value: "456", @@ -29,17 +29,8 @@ class BoxTreeTests: XCTestCase { attributes: [] ) - let boxTree = root.transformToBoxTree() - - guard let foo = boxTree.elements["foo"] as? UnkeyedBox else { - XCTAssert( - false, - """ - flattened.elements["foo"] is not an UnkeyedBox - """ - ) - return - } + let boxTree = try XCTUnwrap(root.transformToBoxTree() as? KeyedBox) + let foo = try XCTUnwrap(boxTree.elements["foo"]) XCTAssertEqual(foo.count, 2) } diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 72af2344..24c8ebe2 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 1482D59E22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D59D22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift */; }; 1482D5A222DD2D9400AE2D6E /* SimpleChoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D5A122DD2D9400AE2D6E /* SimpleChoiceTests.swift */; }; 1482D5A422DD2F4D00AE2D6E /* CompositeChoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D5A322DD2F4D00AE2D6E /* CompositeChoiceTests.swift */; }; + 1482D5A822DD6AEE00AE2D6E /* SingleElementBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1482D5A722DD6AED00AE2D6E /* SingleElementBox.swift */; }; A61DCCD821DF9CA200C0A19D /* ClassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61DCCD621DF8DB300C0A19D /* ClassTests.swift */; }; A61FE03921E4D60B0015D993 /* UnkeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */; }; A61FE03C21E4EAB10015D993 /* KeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */; }; @@ -153,6 +154,7 @@ 1482D59D22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLChoiceEncodable.swift; sourceTree = ""; }; 1482D5A122DD2D9400AE2D6E /* SimpleChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleChoiceTests.swift; sourceTree = ""; }; 1482D5A322DD2F4D00AE2D6E /* CompositeChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositeChoiceTests.swift; sourceTree = ""; }; + 1482D5A722DD6AED00AE2D6E /* SingleElementBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleElementBox.swift; sourceTree = ""; }; A61DCCD621DF8DB300C0A19D /* ClassTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassTests.swift; sourceTree = ""; }; A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnkeyedIntTests.swift; sourceTree = ""; }; A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = ""; }; @@ -310,6 +312,7 @@ BF9457A021CBB497005ACFDE /* UnkeyedBox.swift */, BF94579F21CBB497005ACFDE /* KeyedBox.swift */, BF63EF1721CEB6BD001D38C5 /* SharedBox.swift */, + 1482D5A722DD6AED00AE2D6E /* SingleElementBox.swift */, ); path = Box; sourceTree = ""; @@ -645,6 +648,7 @@ BF9457AE21CBB498005ACFDE /* Box.swift in Sources */, BF9457DA21CBB5D2005ACFDE /* DataBox.swift in Sources */, BF9457AB21CBB498005ACFDE /* DecimalBox.swift in Sources */, + 1482D5A822DD6AEE00AE2D6E /* SingleElementBox.swift in Sources */, OBJ_56 /* XMLKeyedEncodingContainer.swift in Sources */, D158F12F2229892C0032B449 /* DynamicNodeDecoding.swift in Sources */, D1E0C85521D91EBF0042A261 /* Metatypes.swift in Sources */, From bf31592402f1672e4397872aba053ccace7f2ca0 Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 21:55:23 -0700 Subject: [PATCH 08/11] Remove unused changes --- Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift index b2538246..ec0059f3 100644 --- a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift @@ -99,10 +99,10 @@ struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { defer { self.decoder.codingPath.removeLast() } let box = container.withShared { unkeyedBox in - return unkeyedBox[self.currentIndex] + unkeyedBox[self.currentIndex] } - var value = try decode(decoder, box) + let value = try decode(decoder, box) defer { currentIndex += 1 } From f1c7e294c416b39ed8643e7c1b5fdaaba8b2206f Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 21:56:25 -0700 Subject: [PATCH 09/11] Get rid of extra stuff --- .../Auxiliaries/XMLCoderElement.swift | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 37ef3c92..3657ec2a 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -54,23 +54,19 @@ struct XMLCoderElement: Equatable { func transformToBoxTree() -> Box { if let value = value, self.attributes.isEmpty, self.elements.isEmpty { return SingleElementBox(key: key, element: StringBox(value)) - } else { - let attributes = KeyedStorage(self.attributes.map { attribute in - (key: attribute.key, value: StringBox(attribute.value) as SimpleBox) - }) - let storage = KeyedStorage() - var elements = self.elements.reduce(storage) { $0.merge(element: $1) } - - // Handle attributed unkeyed value zap - // Value should be zap. Detect only when no other elements exist - if elements.isEmpty, let value = value { - elements.append(StringBox(value), at: "value") - } - let keyedBox = KeyedBox(elements: elements, attributes: attributes) - - return keyedBox } + let attributes = KeyedStorage(self.attributes.map { attribute in + (key: attribute.key, value: StringBox(attribute.value) as SimpleBox) + }) + let storage = KeyedStorage() + var elements = self.elements.reduce(storage) { $0.merge(element: $1) } + // Handle attributed unkeyed value zap + // Value should be zap. Detect only when no other elements exist + if elements.isEmpty, let value = value { + elements.append(StringBox(value), at: "value") + } + return KeyedBox(elements: elements, attributes: attributes) } func toXMLString(with header: XMLHeader? = nil, From 190ee15d886326b621272b9c673599befda97c4e Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 21:57:28 -0700 Subject: [PATCH 10/11] Whitespace --- Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index 75c8d5aa..3bc5aba5 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -387,7 +387,6 @@ extension XMLDecoderImplementation { } func unbox(_ box: Box) throws -> T { - let decoded: T? let type = T.self From 658bb672c0cf333d34e1afd034d3b7fc0e59ba0e Mon Sep 17 00:00:00 2001 From: James Bean Date: Mon, 15 Jul 2019 21:59:16 -0700 Subject: [PATCH 11/11] Leave comment --- Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index 3bc5aba5..5ce3d2cb 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -377,6 +377,7 @@ extension XMLDecoderImplementation { do { return try unbox(box.element) } catch { + // FIXME: Find a more economical way to unbox a `SingleElementBox` ! return try unbox( KeyedBox( elements: KeyedStorage([(box.key, box.element)]),