From 547d5971f51cc93daad707e5b5c67bda94fed107 Mon Sep 17 00:00:00 2001 From: Pilkington Date: Thu, 9 Aug 2018 14:28:53 -0700 Subject: [PATCH 1/2] Add ListDecodingStrategy and ListEncodingStrategy with an option to not reflect the individual item's tag in the type structure. No change to the default behaviour. --- Sources/XMLCoding/Decoder/XMLDecoder.swift | 18 +++- .../Decoder/XMLUnkeyedDecodingContainer.swift | 15 +++- Sources/XMLCoding/Encoder/XMLEncoder.swift | 32 ++++++- Tests/XMLCodingTests/XMLParsingTests.swift | 83 +++++++++++++++++++ 4 files changed, 144 insertions(+), 4 deletions(-) diff --git a/Sources/XMLCoding/Decoder/XMLDecoder.swift b/Sources/XMLCoding/Decoder/XMLDecoder.swift index dc5cd12..4bc7231 100644 --- a/Sources/XMLCoding/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoding/Decoder/XMLDecoder.swift @@ -102,6 +102,18 @@ open class XMLDecoder { case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String) } + /// The strategy to use when decoding lists. + public enum ListDecodingStrategy { + /// Preserves the XML structure, an outer type will contain lists + /// grouped under the tag used for individual items. This is the default strategy. + case preserveStructure + + /// Collapse the XML structure to avoid the outer type. + /// Useful when individual items will all listed under the one tag and + /// the added layer of the outer type is not useful. + case collapseListUsingItemTag(String) + } + /// The strategy to use in decoding dates. Defaults to `.secondsSince1970`. open var dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970 @@ -111,6 +123,9 @@ open class XMLDecoder { /// The strategy to use in decoding non-conforming numbers. Defaults to `.throw`. open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw + /// The strategy to use in decoding lists. Defaults to `.preserveStructure`. + open var listDecodingStrategy: ListDecodingStrategy = .preserveStructure + /// Contextual user-provided information for use during decoding. open var userInfo: [CodingUserInfoKey : Any] = [:] @@ -119,6 +134,7 @@ open class XMLDecoder { let dateDecodingStrategy: DateDecodingStrategy let dataDecodingStrategy: DataDecodingStrategy let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy + let listDecodingStrategy: ListDecodingStrategy let userInfo: [CodingUserInfoKey : Any] } @@ -127,6 +143,7 @@ open class XMLDecoder { return _Options(dateDecodingStrategy: dateDecodingStrategy, dataDecodingStrategy: dataDecodingStrategy, nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy, + listDecodingStrategy: listDecodingStrategy, userInfo: userInfo) } @@ -610,4 +627,3 @@ extension _XMLDecoder { return decoded } } - diff --git a/Sources/XMLCoding/Decoder/XMLUnkeyedDecodingContainer.swift b/Sources/XMLCoding/Decoder/XMLUnkeyedDecodingContainer.swift index bc15af7..e7c69d8 100644 --- a/Sources/XMLCoding/Decoder/XMLUnkeyedDecodingContainer.swift +++ b/Sources/XMLCoding/Decoder/XMLUnkeyedDecodingContainer.swift @@ -28,9 +28,21 @@ internal struct _XMLUnkeyedDecodingContainer : UnkeyedDecodingContainer { /// Initializes `self` by referencing the given decoder and container. internal init(referencing decoder: _XMLDecoder, wrapping container: [Any]) { self.decoder = decoder - self.container = container self.codingPath = decoder.codingPath self.currentIndex = 0 + + switch decoder.options.listDecodingStrategy { + case .preserveStructure: + self.container = container + case .collapseListUsingItemTag(let itemTag): + if container.count == 1, + let itemKeyMap = container[0] as? [AnyHashable: Any], + let list = itemKeyMap[itemTag] as? [Any] { + self.container = list + } else { + self.container = [] + } + } } // MARK: - UnkeyedDecodingContainer Methods @@ -362,3 +374,4 @@ internal struct _XMLUnkeyedDecodingContainer : UnkeyedDecodingContainer { return _XMLDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options) } } + diff --git a/Sources/XMLCoding/Encoder/XMLEncoder.swift b/Sources/XMLCoding/Encoder/XMLEncoder.swift index 2ab2861..ed05cf6 100644 --- a/Sources/XMLCoding/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoding/Encoder/XMLEncoder.swift @@ -174,6 +174,19 @@ open class XMLEncoder { case custom((Encoder) -> Bool) } + /// The strategy to use when encoding lists. + public enum ListEncodingStrategy { + /// Preserves the type structure. The CodingKey of the List will be used as + /// the tag for each individual item. This is the default strategy. + case preserveStructure + + /// Places the individual items of a list within the specified tag and the + /// CodingKey of the List becomes a single outer tag containing all items. + /// Useful for when you want the XML to have this structure but you don't + /// want the type structure to contain this additional wrapping layer. + case expandListWithItemTag(String) + } + /// The output format to produce. Defaults to `[]`. open var outputFormatting: OutputFormatting = [] @@ -195,6 +208,9 @@ open class XMLEncoder { /// The strategy to use in encoding strings. Defaults to `.deferredToString`. open var stringEncodingStrategy: StringEncodingStrategy = .deferredToString + /// The strategy to use in encoding lists. Defaults to `.preserveStructure`. + open var listEncodingStrategy: ListEncodingStrategy = .preserveStructure + /// Contextual user-provided information for use during encoding. open var userInfo: [CodingUserInfoKey : Any] = [:] @@ -206,6 +222,7 @@ open class XMLEncoder { let keyEncodingStrategy: KeyEncodingStrategy let attributeEncodingStrategy: AttributeEncodingStrategy let stringEncodingStrategy: StringEncodingStrategy + let listEncodingStrategy: ListEncodingStrategy let userInfo: [CodingUserInfoKey : Any] } @@ -217,6 +234,7 @@ open class XMLEncoder { keyEncodingStrategy: keyEncodingStrategy, attributeEncodingStrategy: attributeEncodingStrategy, stringEncodingStrategy: stringEncodingStrategy, + listEncodingStrategy: listEncodingStrategy, userInfo: userInfo) } @@ -317,8 +335,18 @@ internal class _XMLEncoder: Encoder { // If an existing unkeyed container was already requested, return that one. let topContainer: NSMutableArray if self.canEncodeNewValue { - // We haven't yet pushed a container at this level; do so here. - topContainer = self.storage.pushUnkeyedContainer() + switch options.listEncodingStrategy { + case .preserveStructure: + // We haven't yet pushed a container at this level; do so here. + topContainer = self.storage.pushUnkeyedContainer() + case .expandListWithItemTag(let itemTag): + // create an outer keyed container, with a new array as + // its sole entry + let outerContainer = self.storage.pushKeyedContainer() + let array = NSMutableArray() + outerContainer[itemTag] = array + topContainer = array + } } else { guard let container = self.storage.containers.last as? NSMutableArray else { preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.") diff --git a/Tests/XMLCodingTests/XMLParsingTests.swift b/Tests/XMLCodingTests/XMLParsingTests.swift index 81df82c..31b2d68 100644 --- a/Tests/XMLCodingTests/XMLParsingTests.swift +++ b/Tests/XMLCodingTests/XMLParsingTests.swift @@ -1,6 +1,22 @@ import XCTest @testable import XMLCoding +let LIST_XML = """ + + + + + id1 + + + id2 + + + id3 + + + + """ class XMLParsingTests: XCTestCase { struct Result: Codable { @@ -19,6 +35,14 @@ class XMLParsingTests: XCTestCase { } } + struct MetadataList: Codable { + let items: [Metadata] + + enum CodingKeys: String, CodingKey { + case items = "item" + } + } + struct Response: Codable { let result: Result let metadata: Metadata @@ -29,6 +53,26 @@ class XMLParsingTests: XCTestCase { } } + struct ResponseWithList: Codable { + let result: Result + let metadataList: MetadataList + + enum CodingKeys: String, CodingKey { + case result = "Result" + case metadataList = "MetadataList" + } + } + + struct ResponseWithCollapsedList: Codable { + let result: Result + let metadataList: [Metadata] + + enum CodingKeys: String, CodingKey { + case result = "Result" + case metadataList = "MetadataList" + } + } + func testEmptyElement() throws { let inputString = """ @@ -69,9 +113,48 @@ class XMLParsingTests: XCTestCase { XCTAssertEqual("message", response.result.message) } + + func testListDecodingWithDefaultStrategy() throws { + guard let inputData = LIST_XML.data(using: .utf8) else { + return XCTFail() + } + + let response = try XMLDecoder().decode(ResponseWithList.self, from: inputData) + + XCTAssertEqual(3, response.metadataList.items.count) + + // encode the output to make sure we get what we started with + let data = try XMLEncoder().encode(response, withRootKey: "Response") + let encodedString = String(data: data, encoding: .utf8) ?? "" + + XCTAssertEqual(LIST_XML, encodedString) + } + + func testListDecodingWithCollapseItemTagStrategy() throws { + guard let inputData = LIST_XML.data(using: .utf8) else { + return XCTFail() + } + + let decoder = XMLDecoder() + decoder.listDecodingStrategy = .collapseListUsingItemTag("item") + let response = try decoder.decode(ResponseWithCollapsedList.self, from: inputData) + + XCTAssertEqual(3, response.metadataList.count) + + let encoder = XMLEncoder() + encoder.listEncodingStrategy = .expandListWithItemTag("item") + + // encode the output to make sure we get what we started with + let data = try encoder.encode(response, withRootKey: "Response") + let encodedString = String(data: data, encoding: .utf8) ?? "" + + XCTAssertEqual(LIST_XML, encodedString) + } static var allTests = [ ("testEmptyElement", testEmptyElement), ("testEmptyElementNotEffectingPreviousElement", testEmptyElementNotEffectingPreviousElement), + ("testListDecodingWithDefaultStrategy", testListDecodingWithDefaultStrategy), + ("testListDecodingWithCollapseItemTagStrategy", testListDecodingWithCollapseItemTagStrategy) ] } From a8832ffe698f4ffed789d029d990d11b4dbd9420 Mon Sep 17 00:00:00 2001 From: Pilkington Date: Thu, 9 Aug 2018 14:37:25 -0700 Subject: [PATCH 2/2] Clarify the purpose of ListDecodingStrategy.collapseListUsingItemTag. --- Sources/XMLCoding/Decoder/XMLDecoder.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/XMLCoding/Decoder/XMLDecoder.swift b/Sources/XMLCoding/Decoder/XMLDecoder.swift index 4bc7231..d00815f 100644 --- a/Sources/XMLCoding/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoding/Decoder/XMLDecoder.swift @@ -109,8 +109,9 @@ open class XMLDecoder { case preserveStructure /// Collapse the XML structure to avoid the outer type. - /// Useful when individual items will all listed under the one tag and - /// the added layer of the outer type is not useful. + /// Useful when individual items will all be listed under one tag; + /// the outer type will only include one list under this tag and can be + /// omitted. case collapseListUsingItemTag(String) }