diff --git a/Sources/XMLCoding/Decoder/XMLDecoder.swift b/Sources/XMLCoding/Decoder/XMLDecoder.swift
index 1002286..7d577fe 100644
--- a/Sources/XMLCoding/Decoder/XMLDecoder.swift
+++ b/Sources/XMLCoding/Decoder/XMLDecoder.swift
@@ -115,6 +115,31 @@ open class XMLDecoder {
case collapseListUsingItemTag(String)
}
+ /// The strategy to use when decoding maps.
+ public enum MapDecodingStrategy {
+ /// Preserves the XML structure. Key and Value tags will be retained producing a
+ /// list of elements with these two attributes. This is the default strategy.
+ case preserveStructure
+
+ /// Collapse the XML structure to avoid key and value tags. For example-
+ ///
+ ///
+ /// QueueType
+ /// Production
+ ///
+ ///
+ /// Owner
+ /// Developer123
+ ///
+ ///
+ ///
+ /// will be decoded into
+ /// struct Result {
+ /// let tags: [String: String]
+ /// }
+ case collapseMapUsingTags(keyTag: String, valueTag: String)
+ }
+
/// The strategy to use in decoding dates. Defaults to `.secondsSince1970`.
open var dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970
@@ -127,6 +152,9 @@ open class XMLDecoder {
/// The strategy to use in decoding lists. Defaults to `.preserveStructure`.
open var listDecodingStrategy: ListDecodingStrategy = .preserveStructure
+ /// The strategy to use in decoding maps. Defaults to `.preserveStructure`.
+ open var mapDecodingStrategy: MapDecodingStrategy = .preserveStructure
+
/// Contextual user-provided information for use during decoding.
open var userInfo: [CodingUserInfoKey : Any] = [:]
@@ -136,6 +164,7 @@ open class XMLDecoder {
let dataDecodingStrategy: DataDecodingStrategy
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
let listDecodingStrategy: ListDecodingStrategy
+ let mapDecodingStrategy: MapDecodingStrategy
let userInfo: [CodingUserInfoKey : Any]
}
@@ -145,6 +174,7 @@ open class XMLDecoder {
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
listDecodingStrategy: listDecodingStrategy,
+ mapDecodingStrategy: mapDecodingStrategy,
userInfo: userInfo)
}
@@ -209,6 +239,21 @@ internal class _XMLDecoder : Decoder {
// MARK: - Decoder Methods
+ private func getMapFromListOfEntries(entryList: [Any], keyTag: String, valueTag: String) -> [String : Any] {
+ var newContainer: [String: Any] = [:]
+
+ // construct a dictionary from each entry and the key and value tags
+ entryList.forEach { entry in
+ if let keyedContainer = entry as? [String : Any],
+ let key = keyedContainer[keyTag] as? String,
+ let value = keyedContainer[valueTag] {
+ newContainer[key] = value
+ }
+ }
+
+ return newContainer
+ }
+
public func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer {
guard !(self.storage.topContainer is NSNull) else {
throw DecodingError.valueNotFound(KeyedDecodingContainer.self,
@@ -216,7 +261,23 @@ internal class _XMLDecoder : Decoder {
debugDescription: "Cannot get keyed decoding container -- found null value instead."))
}
- guard let topContainer = self.storage.topContainer as? [String : Any] else {
+ let topContainer: [String : Any]
+ // if this is a dictionary
+ if let currentContainer = self.storage.topContainer as? [String : Any] {
+
+ // if we are combining collapsing lists and maps
+ if case let .collapseListUsingItemTag(itemTag) = options.listDecodingStrategy,
+ case let .collapseMapUsingTags(keyTag: keyTag, valueTag: valueTag) = options.mapDecodingStrategy,
+ let itemList = currentContainer[itemTag] as? [Any] {
+ topContainer = getMapFromListOfEntries(entryList: itemList, keyTag: keyTag, valueTag: valueTag)
+ } else {
+ topContainer = currentContainer
+ }
+ // if this is a list and the mapDecodingStrategy is collapseMapUsingTags
+ } else if let currentContainer = self.storage.topContainer as? [Any],
+ case let .collapseMapUsingTags(keyTag: keyTag, valueTag: valueTag) = options.mapDecodingStrategy {
+ topContainer = getMapFromListOfEntries(entryList: currentContainer, keyTag: keyTag, valueTag: valueTag)
+ } else {
throw DecodingError._typeMismatch(at: self.codingPath, expectation: [String : Any].self, reality: self.storage.topContainer)
}
diff --git a/Sources/XMLCoding/Encoder/XMLEncoder.swift b/Sources/XMLCoding/Encoder/XMLEncoder.swift
index b3c2a8e..6a36a0b 100644
--- a/Sources/XMLCoding/Encoder/XMLEncoder.swift
+++ b/Sources/XMLCoding/Encoder/XMLEncoder.swift
@@ -187,6 +187,31 @@ open class XMLEncoder {
case expandListWithItemTag(String)
}
+ /// The strategy to use when encoding maps.
+ public enum MapEncodingStrategy {
+ /// Preserves the XML structure. Key and Value tags will be retained producing a
+ /// list of elements with these two attributes. This is the default strategy.
+ case preserveStructure
+
+ /// Expands the XML structure to avoid key and value tags. For example-
+ ///
+ ///
+ /// QueueType
+ /// Production
+ ///
+ ///
+ /// Owner
+ /// Developer123
+ ///
+ ///
+ ///
+ /// will be encoded from
+ /// struct Result {
+ /// let tags: [String: String]
+ /// }
+ case expandMapUsingTags(keyTag: String, valueTag: String)
+ }
+
/// The output format to produce. Defaults to `[]`.
open var outputFormatting: OutputFormatting = []
@@ -211,6 +236,9 @@ open class XMLEncoder {
/// The strategy to use in encoding lists. Defaults to `.preserveStructure`.
open var listEncodingStrategy: ListEncodingStrategy = .preserveStructure
+ /// The strategy to use in encoding map. Defaults to `.preserveStructure`.
+ open var mapEncodingStrategy: MapEncodingStrategy = .preserveStructure
+
/// Contextual user-provided information for use during encoding.
open var userInfo: [CodingUserInfoKey : Any] = [:]
@@ -223,6 +251,7 @@ open class XMLEncoder {
let attributeEncodingStrategy: AttributeEncodingStrategy
let stringEncodingStrategy: StringEncodingStrategy
let listEncodingStrategy: ListEncodingStrategy
+ let mapEncodingStrategy: MapEncodingStrategy
let userInfo: [CodingUserInfoKey : Any]
}
@@ -235,6 +264,7 @@ open class XMLEncoder {
attributeEncodingStrategy: attributeEncodingStrategy,
stringEncodingStrategy: stringEncodingStrategy,
listEncodingStrategy: listEncodingStrategy,
+ mapEncodingStrategy: mapEncodingStrategy,
userInfo: userInfo)
}
@@ -936,6 +966,9 @@ extension _XMLEncoder {
return .uint64(UInt64(int))
} else if let int = value as? UInt32 {
return .uint64(UInt64(int))
+ } else if let boxableEncodable = value as? BoxableEncodable {
+ // this type knows how to box itself
+ return try boxableEncodable.box(forEncoder: self)
}
let depth = self.storage.count
@@ -950,3 +983,58 @@ extension _XMLEncoder {
}
}
+/// Protocol for a type that knows how to box itself
+protocol BoxableEncodable: Encodable {
+ func box(forEncoder encoder: _XMLEncoder) throws -> MutableContainer?
+}
+
+extension Dictionary: BoxableEncodable where Key == String, Value: Encodable {
+ /// function to box (and potentially expand
+ internal func box(forEncoder encoder: _XMLEncoder) throws -> MutableContainer? {
+ let depth = encoder.storage.count
+ // if we are expanding maps
+ if case let .expandMapUsingTags(keyTag: keyTag, valueTag: valueTag) = encoder.options.mapEncodingStrategy {
+ let outerMutableContainer: (MutableContainerDictionary, String)?
+ // if we are expanding lists, wrap everything in a dictionary with that item
+ if case let .expandListWithItemTag(itemTag) = encoder.options.listEncodingStrategy {
+ let newMutableContainerDictionary = MutableContainerDictionary()
+ encoder.storage.push(container: .dictionary(newMutableContainerDictionary))
+ outerMutableContainer = (newMutableContainerDictionary, itemTag)
+ } else {
+ outerMutableContainer = nil
+ }
+
+ // create the expanded array
+ let mutableContainerArray = MutableContainerArray()
+ encoder.storage.push(container: .array(mutableContainerArray))
+
+ try self.forEach { (key, value) in
+ // create the dictionary for the key and value entries
+ let mutableContainerDictionary = MutableContainerDictionary()
+ encoder.storage.push(container: .dictionary(mutableContainerDictionary))
+ mutableContainerDictionary[keyTag] = .string(key)
+ mutableContainerDictionary[valueTag] = try encoder.box(value)
+
+ // add to the array
+ mutableContainerArray.append(encoder.storage.popContainer())
+ }
+
+ // add to the outer container if it is needed
+ if let outerMutableContainer = outerMutableContainer {
+ outerMutableContainer.0[outerMutableContainer.1] = encoder.storage.popContainer()
+ }
+ } else {
+ // otherwise just encode this array as normal
+ try self.encode(to: encoder)
+ }
+
+ // The top container should be a new container.
+ guard encoder.storage.count > depth else {
+ return nil
+ }
+
+ // return what has been created
+ return encoder.storage.popContainer()
+ }
+}
+
diff --git a/Tests/XMLCodingTests/XMLParsingTests.swift b/Tests/XMLCodingTests/XMLParsingTests.swift
index 1522deb..1998e57 100644
--- a/Tests/XMLCodingTests/XMLParsingTests.swift
+++ b/Tests/XMLCodingTests/XMLParsingTests.swift
@@ -18,6 +18,26 @@ let LIST_XML = """
"""
+let MAP_XML = """
+
+
+
+ -
+ key1
+ value1
+
+ -
+ key2
+ value2
+
+ -
+ key3
+ value3
+
+
+
+ """
+
let SINGLETON_LIST_XML = """
@@ -58,6 +78,17 @@ class XMLParsingTests: XCTestCase {
}
}
+ struct MapEntry: Codable, Equatable {
+ let key: String
+ let value: String
+
+
+ enum CodingKeys: String, CodingKey {
+ case key = "Key"
+ case value = "Value"
+ }
+ }
+
struct MetadataWithData: Codable, Equatable {
let id: String
let data: Data
@@ -76,6 +107,22 @@ class XMLParsingTests: XCTestCase {
}
}
+ struct MetadataMap: Codable, Equatable {
+ let items: [MapEntry]
+
+ enum CodingKeys: String, CodingKey {
+ case items = "item"
+ }
+ }
+
+ struct MetadataWithCollapsedMap: Codable, Equatable {
+ let items: [String: String]
+
+ enum CodingKeys: String, CodingKey {
+ case items = "item"
+ }
+ }
+
struct Response: Codable, Equatable {
let result: Result
let metadata: Metadata
@@ -106,6 +153,36 @@ class XMLParsingTests: XCTestCase {
}
}
+ struct ResponseWithMap: Codable, Equatable {
+ let result: Result
+ let metadataMap: MetadataMap
+
+ enum CodingKeys: String, CodingKey {
+ case result = "Result"
+ case metadataMap = "MetadataMap"
+ }
+ }
+
+ struct ResponseWithCollapsedMap: Codable, Equatable {
+ let result: Result
+ let metadataMap: MetadataWithCollapsedMap
+
+ enum CodingKeys: String, CodingKey {
+ case result = "Result"
+ case metadataMap = "MetadataMap"
+ }
+ }
+
+ struct ResponseWithCollapsedListAndMap: Codable, Equatable {
+ let result: Result
+ let metadataMap: [String: String]
+
+ enum CodingKeys: String, CodingKey {
+ case result = "Result"
+ case metadataMap = "MetadataMap"
+ }
+ }
+
struct ResponseWithCollapsedList: Codable, Equatable {
let result: Result
let metadataList: [Metadata]
@@ -463,6 +540,68 @@ class XMLParsingTests: XCTestCase {
XCTAssertEqual(response, response2)
}
+ /// Test that we can decode/encode maps with the default strategy
+ func testMapDecodingWithDefaultStrategy() throws {
+ guard let inputData = MAP_XML.data(using: .utf8) else {
+ return XCTFail()
+ }
+
+ let response = try XMLDecoder().decode(ResponseWithMap.self, from: inputData)
+
+ XCTAssertEqual(3, response.metadataMap.items.count)
+
+ // encode the output to make sure we get what we started with
+ let encoder = XMLEncoder()
+ let data = try encoder.encode(response, withRootKey: "Response")
+
+ let response2 = try XMLDecoder().decode(ResponseWithMap.self, from: data)
+ XCTAssertEqual(response, response2)
+ }
+
+ /// Test that we can decode/encode maps with collapsed maps
+ func testMapDecodingWithCollapsedMap() throws {
+ guard let inputData = MAP_XML.data(using: .utf8) else {
+ return XCTFail()
+ }
+
+ let decoder = XMLDecoder()
+ decoder.mapDecodingStrategy = .collapseMapUsingTags(keyTag: "Key", valueTag: "Value")
+ let response = try! decoder.decode(ResponseWithCollapsedMap.self, from: inputData)
+
+ XCTAssertEqual(3, response.metadataMap.items.count)
+
+ // encode the output to make sure we get what we started with
+ let encoder = XMLEncoder()
+ encoder.mapEncodingStrategy = .expandMapUsingTags(keyTag: "Key", valueTag: "Value")
+ let data = try encoder.encode(response, withRootKey: "Response")
+
+ let response2 = try decoder.decode(ResponseWithCollapsedMap.self, from: data)
+ XCTAssertEqual(response, response2)
+ }
+
+ /// Test that we can decode/encode maps with collapsed lists and maps
+ func testMapDecodingWithCollapsedListAndMap() throws {
+ guard let inputData = MAP_XML.data(using: .utf8) else {
+ return XCTFail()
+ }
+
+ let decoder = XMLDecoder()
+ decoder.mapDecodingStrategy = .collapseMapUsingTags(keyTag: "Key", valueTag: "Value")
+ decoder.listDecodingStrategy = .collapseListUsingItemTag("item")
+ let response = try! decoder.decode(ResponseWithCollapsedListAndMap.self, from: inputData)
+
+ XCTAssertEqual(3, response.metadataMap.count)
+
+ // encode the output to make sure we get what we started with
+ let encoder = XMLEncoder()
+ encoder.mapEncodingStrategy = .expandMapUsingTags(keyTag: "Key", valueTag: "Value")
+ encoder.listEncodingStrategy = .expandListWithItemTag("item")
+ let data = try encoder.encode(response, withRootKey: "Response")
+
+ let response2 = try decoder.decode(ResponseWithCollapsedListAndMap.self, from: data)
+ XCTAssertEqual(response, response2)
+ }
+
/// Test that we can decode/encode single element lists with the default strategy
func testSingletonListDecodingWithDefaultStrategy() throws {
guard let inputData = SINGLETON_LIST_XML.data(using: .utf8) else {
@@ -536,6 +675,9 @@ class XMLParsingTests: XCTestCase {
("testEmptyStructureElement", testEmptyStructureElement),
("testEmptyStructureElementNotEffectingPreviousElement", testEmptyStructureElementNotEffectingPreviousElement),
("testListDecodingWithDefaultStrategy", testListDecodingWithDefaultStrategy),
+ ("testMapDecodingWithDefaultStrategy", testMapDecodingWithDefaultStrategy),
+ ("testMapDecodingWithCollapsedMap", testMapDecodingWithCollapsedMap),
+ ("testMapDecodingWithCollapsedListAndMap", testMapDecodingWithCollapsedListAndMap),
("testSingletonListDecodingWithDefaultStrategy", testSingletonListDecodingWithDefaultStrategy),
("testListDecodingWithCollapseItemTagStrategy", testListDecodingWithCollapseItemTagStrategy),
("testSingletonListDecodingWithCollapseItemTagStrategy", testSingletonListDecodingWithCollapseItemTagStrategy)