Skip to content

Commit f5d46f4

Browse files
authored
Merge pull request #2 from LiveUI/listStrategy
Add ListEncodingStrategy and ListDecodingStrategy
2 parents a4475f1 + a8832ff commit f5d46f4

4 files changed

Lines changed: 145 additions & 4 deletions

File tree

Sources/XMLCoding/Decoder/XMLDecoder.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,19 @@ open class XMLDecoder {
102102
case convertFromString(positiveInfinity: String, negativeInfinity: String, nan: String)
103103
}
104104

105+
/// The strategy to use when decoding lists.
106+
public enum ListDecodingStrategy {
107+
/// Preserves the XML structure, an outer type will contain lists
108+
/// grouped under the tag used for individual items. This is the default strategy.
109+
case preserveStructure
110+
111+
/// Collapse the XML structure to avoid the outer type.
112+
/// Useful when individual items will all be listed under one tag;
113+
/// the outer type will only include one list under this tag and can be
114+
/// omitted.
115+
case collapseListUsingItemTag(String)
116+
}
117+
105118
/// The strategy to use in decoding dates. Defaults to `.secondsSince1970`.
106119
open var dateDecodingStrategy: DateDecodingStrategy = .secondsSince1970
107120

@@ -111,6 +124,9 @@ open class XMLDecoder {
111124
/// The strategy to use in decoding non-conforming numbers. Defaults to `.throw`.
112125
open var nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy = .throw
113126

127+
/// The strategy to use in decoding lists. Defaults to `.preserveStructure`.
128+
open var listDecodingStrategy: ListDecodingStrategy = .preserveStructure
129+
114130
/// Contextual user-provided information for use during decoding.
115131
open var userInfo: [CodingUserInfoKey : Any] = [:]
116132

@@ -119,6 +135,7 @@ open class XMLDecoder {
119135
let dateDecodingStrategy: DateDecodingStrategy
120136
let dataDecodingStrategy: DataDecodingStrategy
121137
let nonConformingFloatDecodingStrategy: NonConformingFloatDecodingStrategy
138+
let listDecodingStrategy: ListDecodingStrategy
122139
let userInfo: [CodingUserInfoKey : Any]
123140
}
124141

@@ -127,6 +144,7 @@ open class XMLDecoder {
127144
return _Options(dateDecodingStrategy: dateDecodingStrategy,
128145
dataDecodingStrategy: dataDecodingStrategy,
129146
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
147+
listDecodingStrategy: listDecodingStrategy,
130148
userInfo: userInfo)
131149
}
132150

@@ -610,4 +628,3 @@ extension _XMLDecoder {
610628
return decoded
611629
}
612630
}
613-

Sources/XMLCoding/Decoder/XMLUnkeyedDecodingContainer.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,21 @@ internal struct _XMLUnkeyedDecodingContainer : UnkeyedDecodingContainer {
2828
/// Initializes `self` by referencing the given decoder and container.
2929
internal init(referencing decoder: _XMLDecoder, wrapping container: [Any]) {
3030
self.decoder = decoder
31-
self.container = container
3231
self.codingPath = decoder.codingPath
3332
self.currentIndex = 0
33+
34+
switch decoder.options.listDecodingStrategy {
35+
case .preserveStructure:
36+
self.container = container
37+
case .collapseListUsingItemTag(let itemTag):
38+
if container.count == 1,
39+
let itemKeyMap = container[0] as? [AnyHashable: Any],
40+
let list = itemKeyMap[itemTag] as? [Any] {
41+
self.container = list
42+
} else {
43+
self.container = []
44+
}
45+
}
3446
}
3547

3648
// MARK: - UnkeyedDecodingContainer Methods
@@ -362,3 +374,4 @@ internal struct _XMLUnkeyedDecodingContainer : UnkeyedDecodingContainer {
362374
return _XMLDecoder(referencing: value, at: self.decoder.codingPath, options: self.decoder.options)
363375
}
364376
}
377+

Sources/XMLCoding/Encoder/XMLEncoder.swift

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,19 @@ open class XMLEncoder {
174174
case custom((Encoder) -> Bool)
175175
}
176176

177+
/// The strategy to use when encoding lists.
178+
public enum ListEncodingStrategy {
179+
/// Preserves the type structure. The CodingKey of the List will be used as
180+
/// the tag for each individual item. This is the default strategy.
181+
case preserveStructure
182+
183+
/// Places the individual items of a list within the specified tag and the
184+
/// CodingKey of the List becomes a single outer tag containing all items.
185+
/// Useful for when you want the XML to have this structure but you don't
186+
/// want the type structure to contain this additional wrapping layer.
187+
case expandListWithItemTag(String)
188+
}
189+
177190
/// The output format to produce. Defaults to `[]`.
178191
open var outputFormatting: OutputFormatting = []
179192

@@ -195,6 +208,9 @@ open class XMLEncoder {
195208
/// The strategy to use in encoding strings. Defaults to `.deferredToString`.
196209
open var stringEncodingStrategy: StringEncodingStrategy = .deferredToString
197210

211+
/// The strategy to use in encoding lists. Defaults to `.preserveStructure`.
212+
open var listEncodingStrategy: ListEncodingStrategy = .preserveStructure
213+
198214
/// Contextual user-provided information for use during encoding.
199215
open var userInfo: [CodingUserInfoKey : Any] = [:]
200216

@@ -206,6 +222,7 @@ open class XMLEncoder {
206222
let keyEncodingStrategy: KeyEncodingStrategy
207223
let attributeEncodingStrategy: AttributeEncodingStrategy
208224
let stringEncodingStrategy: StringEncodingStrategy
225+
let listEncodingStrategy: ListEncodingStrategy
209226
let userInfo: [CodingUserInfoKey : Any]
210227
}
211228

@@ -217,6 +234,7 @@ open class XMLEncoder {
217234
keyEncodingStrategy: keyEncodingStrategy,
218235
attributeEncodingStrategy: attributeEncodingStrategy,
219236
stringEncodingStrategy: stringEncodingStrategy,
237+
listEncodingStrategy: listEncodingStrategy,
220238
userInfo: userInfo)
221239
}
222240

@@ -317,8 +335,18 @@ internal class _XMLEncoder: Encoder {
317335
// If an existing unkeyed container was already requested, return that one.
318336
let topContainer: NSMutableArray
319337
if self.canEncodeNewValue {
320-
// We haven't yet pushed a container at this level; do so here.
321-
topContainer = self.storage.pushUnkeyedContainer()
338+
switch options.listEncodingStrategy {
339+
case .preserveStructure:
340+
// We haven't yet pushed a container at this level; do so here.
341+
topContainer = self.storage.pushUnkeyedContainer()
342+
case .expandListWithItemTag(let itemTag):
343+
// create an outer keyed container, with a new array as
344+
// its sole entry
345+
let outerContainer = self.storage.pushKeyedContainer()
346+
let array = NSMutableArray()
347+
outerContainer[itemTag] = array
348+
topContainer = array
349+
}
322350
} else {
323351
guard let container = self.storage.containers.last as? NSMutableArray else {
324352
preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.")

Tests/XMLCodingTests/XMLParsingTests.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
11
import XCTest
22
@testable import XMLCoding
33

4+
let LIST_XML = """
5+
<Response>
6+
<Result />
7+
<MetadataList>
8+
<item>
9+
<Id>id1</Id>
10+
</item>
11+
<item>
12+
<Id>id2</Id>
13+
</item>
14+
<item>
15+
<Id>id3</Id>
16+
</item>
17+
</MetadataList>
18+
</Response>
19+
"""
420

521
class XMLParsingTests: XCTestCase {
622
struct Result: Codable {
@@ -19,6 +35,14 @@ class XMLParsingTests: XCTestCase {
1935
}
2036
}
2137

38+
struct MetadataList: Codable {
39+
let items: [Metadata]
40+
41+
enum CodingKeys: String, CodingKey {
42+
case items = "item"
43+
}
44+
}
45+
2246
struct Response: Codable {
2347
let result: Result
2448
let metadata: Metadata
@@ -29,6 +53,26 @@ class XMLParsingTests: XCTestCase {
2953
}
3054
}
3155

56+
struct ResponseWithList: Codable {
57+
let result: Result
58+
let metadataList: MetadataList
59+
60+
enum CodingKeys: String, CodingKey {
61+
case result = "Result"
62+
case metadataList = "MetadataList"
63+
}
64+
}
65+
66+
struct ResponseWithCollapsedList: Codable {
67+
let result: Result
68+
let metadataList: [Metadata]
69+
70+
enum CodingKeys: String, CodingKey {
71+
case result = "Result"
72+
case metadataList = "MetadataList"
73+
}
74+
}
75+
3276
func testEmptyElement() throws {
3377
let inputString = """
3478
<Response>
@@ -69,9 +113,48 @@ class XMLParsingTests: XCTestCase {
69113

70114
XCTAssertEqual("message", response.result.message)
71115
}
116+
117+
func testListDecodingWithDefaultStrategy() throws {
118+
guard let inputData = LIST_XML.data(using: .utf8) else {
119+
return XCTFail()
120+
}
121+
122+
let response = try XMLDecoder().decode(ResponseWithList.self, from: inputData)
123+
124+
XCTAssertEqual(3, response.metadataList.items.count)
125+
126+
// encode the output to make sure we get what we started with
127+
let data = try XMLEncoder().encode(response, withRootKey: "Response")
128+
let encodedString = String(data: data, encoding: .utf8) ?? ""
129+
130+
XCTAssertEqual(LIST_XML, encodedString)
131+
}
132+
133+
func testListDecodingWithCollapseItemTagStrategy() throws {
134+
guard let inputData = LIST_XML.data(using: .utf8) else {
135+
return XCTFail()
136+
}
137+
138+
let decoder = XMLDecoder()
139+
decoder.listDecodingStrategy = .collapseListUsingItemTag("item")
140+
let response = try decoder.decode(ResponseWithCollapsedList.self, from: inputData)
141+
142+
XCTAssertEqual(3, response.metadataList.count)
143+
144+
let encoder = XMLEncoder()
145+
encoder.listEncodingStrategy = .expandListWithItemTag("item")
146+
147+
// encode the output to make sure we get what we started with
148+
let data = try encoder.encode(response, withRootKey: "Response")
149+
let encodedString = String(data: data, encoding: .utf8) ?? ""
150+
151+
XCTAssertEqual(LIST_XML, encodedString)
152+
}
72153

73154
static var allTests = [
74155
("testEmptyElement", testEmptyElement),
75156
("testEmptyElementNotEffectingPreviousElement", testEmptyElementNotEffectingPreviousElement),
157+
("testListDecodingWithDefaultStrategy", testListDecodingWithDefaultStrategy),
158+
("testListDecodingWithCollapseItemTagStrategy", testListDecodingWithCollapseItemTagStrategy)
76159
]
77160
}

0 commit comments

Comments
 (0)