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 acc800a8..3657ec2a 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -51,7 +51,10 @@ struct XMLCoderElement: Equatable { elements.append(element) } - func transformToBoxTree() -> KeyedBox { + func transformToBoxTree() -> Box { + if let value = value, self.attributes.isEmpty, self.elements.isEmpty { + return SingleElementBox(key: key, element: StringBox(value)) + } let attributes = KeyedStorage(self.attributes.map { attribute in (key: attribute.key, value: StringBox(attribute.value) as SimpleBox) }) @@ -63,9 +66,7 @@ struct XMLCoderElement: Equatable { if elements.isEmpty, let value = value { elements.append(StringBox(value), at: "value") } - let keyedBox = KeyedBox(elements: elements, attributes: attributes) - - return keyedBox + return KeyedBox(elements: elements, attributes: attributes) } 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 fe237eba..5ce3d2cb 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -138,9 +138,10 @@ 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] })) + return XMLUnkeyedDecodingContainer( + referencing: self, + wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleElementBox.init) }) + ) default: throw DecodingError.typeMismatch( at: codingPath, @@ -372,6 +373,20 @@ extension XMLDecoderImplementation { return urlBox.unboxed } + func unbox(_ box: SingleElementBox) throws -> T { + 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)]), + attributes: [] + ) + ) + } + } + func unbox(_ box: Box) throws -> T { let decoded: T? let type = T.self @@ -392,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/Tests/XMLCoderTests/CompositeChoiceTests.swift b/Tests/XMLCoderTests/CompositeChoiceTests.swift index c1c9c2f1..a31662d2 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 @@ -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/Tests/XMLCoderTests/SimpleChoiceTests.swift b/Tests/XMLCoderTests/SimpleChoiceTests.swift index 9c3ab4b7..34bd4974 100644 --- a/Tests/XMLCoderTests/SimpleChoiceTests.swift +++ b/Tests/XMLCoderTests/SimpleChoiceTests.swift @@ -13,7 +13,8 @@ private enum IntOrString: Equatable { case string(String) } -extension IntOrString: Codable { +extension IntOrString: XMLChoiceCodable { + enum CodingKeys: String, CodingKey { case int case string 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 */,