Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion Sources/XMLCoding/Decoder/XMLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,19 @@ 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 be listed under one tag;
/// the outer type will only include one list under this tag and can be
/// omitted.
case collapseListUsingItemTag(String)
}

/// The strategy to use in decoding dates. Defaults to `.secondsSince1970`.
open var dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970

Expand All @@ -111,6 +124,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] = [:]

Expand All @@ -119,6 +135,7 @@ open class XMLDecoder {
let dateDecodingStrategy: DateDecodingStrategy
let dataDecodingStrategy: DataDecodingStrategy
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
let listDecodingStrategy: ListDecodingStrategy
let userInfo: [CodingUserInfoKey : Any]
}

Expand All @@ -127,6 +144,7 @@ open class XMLDecoder {
return _Options(dateDecodingStrategy: dateDecodingStrategy,
dataDecodingStrategy: dataDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
listDecodingStrategy: listDecodingStrategy,
userInfo: userInfo)
}

Expand Down Expand Up @@ -610,4 +628,3 @@ extension _XMLDecoder {
return decoded
}
}

15 changes: 14 additions & 1 deletion Sources/XMLCoding/Decoder/XMLUnkeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -362,3 +374,4 @@ internal struct _XMLUnkeyedDecodingContainer : UnkeyedDecodingContainer {
return _XMLDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options)
}
}

32 changes: 30 additions & 2 deletions Sources/XMLCoding/Encoder/XMLEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []

Expand All @@ -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] = [:]

Expand All @@ -206,6 +222,7 @@ open class XMLEncoder {
let keyEncodingStrategy: KeyEncodingStrategy
let attributeEncodingStrategy: AttributeEncodingStrategy
let stringEncodingStrategy: StringEncodingStrategy
let listEncodingStrategy: ListEncodingStrategy
let userInfo: [CodingUserInfoKey : Any]
}

Expand All @@ -217,6 +234,7 @@ open class XMLEncoder {
keyEncodingStrategy: keyEncodingStrategy,
attributeEncodingStrategy: attributeEncodingStrategy,
stringEncodingStrategy: stringEncodingStrategy,
listEncodingStrategy: listEncodingStrategy,
userInfo: userInfo)
}

Expand Down Expand Up @@ -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.")
Expand Down
83 changes: 83 additions & 0 deletions Tests/XMLCodingTests/XMLParsingTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import XCTest
@testable import XMLCoding

let LIST_XML = """
<Response>
<Result />
<MetadataList>
<item>
<Id>id1</Id>
</item>
<item>
<Id>id2</Id>
</item>
<item>
<Id>id3</Id>
</item>
</MetadataList>
</Response>
"""

class XMLParsingTests: XCTestCase {
struct Result: Codable {
Expand All @@ -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
Expand All @@ -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 = """
<Response>
Expand Down Expand Up @@ -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)
]
}