Skip to content

Commit

Permalink
Merge pull request #2 from LiveUI/listStrategy
Browse files Browse the repository at this point in the history
Add ListEncodingStrategy and ListDecodingStrategy
  • Loading branch information
rafiki270 authored Aug 10, 2018
2 parents a4475f1 + a8832ff commit f5d46f4
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 4 deletions.
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)
]
}

0 comments on commit f5d46f4

Please sign in to comment.