Skip to content
25 changes: 25 additions & 0 deletions Sources/XMLCoder/Auxiliaries/Box/SingleElementBox.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
9 changes: 5 additions & 4 deletions Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion Sources/XMLCoder/Auxiliaries/XMLStackParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
23 changes: 20 additions & 3 deletions Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,10 @@ class XMLDecoderImplementation: Decoder {
case let unkeyed as SharedBox<UnkeyedBox>:
return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
case let keyed as SharedBox<KeyedBox>:
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,
Expand Down Expand Up @@ -372,6 +373,20 @@ extension XMLDecoderImplementation {
return urlBox.unboxed
}

func unbox<T: Decodable>(_ 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<T: Decodable>(_ box: Box) throws -> T {
let decoded: T?
let type = T.self
Expand All @@ -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 {
Expand Down
4 changes: 1 addition & 3 deletions Tests/XMLCoderTests/CompositeChoiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ private enum IntOrStringWrapper: Equatable {
case string(StringWrapper)
}

extension IntOrStringWrapper: Codable {
extension IntOrStringWrapper: XMLChoiceCodable {

enum CodingKeys: String, CodingKey {
case int
Expand Down Expand Up @@ -85,6 +85,4 @@ class CompositeChoiceTests: XCTestCase {
]
XCTAssertEqual(result, expected)
}

#warning("TODO: Add encoding and round-trip tests")
}
15 changes: 3 additions & 12 deletions Tests/XMLCoderTests/Minimal/BoxTreeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import XCTest
@testable import XMLCoder

class BoxTreeTests: XCTestCase {
func testNestedValues() {
func testNestedValues() throws {
let e1 = XMLCoderElement(
key: "foo",
value: "456",
Expand All @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion Tests/XMLCoderTests/SimpleChoiceTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions XMLCoder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -153,6 +154,7 @@
1482D59D22DD2A6B00AE2D6E /* XMLChoiceEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLChoiceEncodable.swift; sourceTree = "<group>"; };
1482D5A122DD2D9400AE2D6E /* SimpleChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleChoiceTests.swift; sourceTree = "<group>"; };
1482D5A322DD2F4D00AE2D6E /* CompositeChoiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositeChoiceTests.swift; sourceTree = "<group>"; };
1482D5A722DD6AED00AE2D6E /* SingleElementBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleElementBox.swift; sourceTree = "<group>"; };
A61DCCD621DF8DB300C0A19D /* ClassTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassTests.swift; sourceTree = "<group>"; };
A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnkeyedIntTests.swift; sourceTree = "<group>"; };
A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -310,6 +312,7 @@
BF9457A021CBB497005ACFDE /* UnkeyedBox.swift */,
BF94579F21CBB497005ACFDE /* KeyedBox.swift */,
BF63EF1721CEB6BD001D38C5 /* SharedBox.swift */,
1482D5A722DD6AED00AE2D6E /* SingleElementBox.swift */,
);
path = Box;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down