Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Encodable and Decodable support for choice elements #119

Merged
merged 35 commits into from
Jul 30, 2019
Merged
Changes from 25 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
749e0fc
Add testing preliminaries for choice elements
bwetherfield Jul 15, 2019
ffbb395
Add ChoiceKey protocol conforming to CodingKey
jsbean Jul 26, 2019
77f9699
Implement choice element encoding
bwetherfield Jul 26, 2019
c128f28
Implement choice element decoding
jsbean Jul 27, 2019
312c4f5
Refactor clean up choice coding implementation
jsbean Jul 27, 2019
91693e9
Rename XMLChoiceKey -> XMLChoiceCodingKey
jsbean Jul 27, 2019
b5684f3
Rename SingleElementBox to SingleKeyedBox
jsbean Jul 26, 2019
5fd8d8f
Rename nestedSingleElementContainer -> nestedChoiceContainer
jsbean Jul 27, 2019
5bda791
Cull redundancies
jsbean Jul 27, 2019
045e07c
Add enum with associated value encoding tests
bwetherfield Jul 27, 2019
a7fb985
Fix usage to one key in the XMLChoiceDecodingContainer
jsbean Jul 26, 2019
b1b6c27
Factor out mapKeys to XMLDecoderImplementation.transformKeyedContainer
jsbean Jul 26, 2019
048d0c3
Be more assertive in NestingTests (#44)
jsbean Jul 27, 2019
5748eee
Merge branch 'master' into choice-implementation
jsbean Jul 27, 2019
c6ee065
Use KeyedBox like we used to (#46)
jsbean Jul 27, 2019
e6467d5
Rename scheme XMLCoder-Package -> XMLCoder
jsbean Jul 28, 2019
ce09102
Share scheme
jsbean Jul 28, 2019
f09c79d
Use Swift 4.2
jsbean Jul 28, 2019
0414fd8
Use Swift 4.2 everywhere
jsbean Jul 28, 2019
e1f0c45
Bring back old performance testing baseline
jsbean Jul 29, 2019
0b9c5cc
Whitespace
jsbean Jul 29, 2019
a8125e2
Bring back scheme management plist
jsbean Jul 29, 2019
bf52ca8
Bring back in empty AdditionalOptions
jsbean Jul 29, 2019
fd594fd
Whitespace
jsbean Jul 29, 2019
a930d00
Remove print statement
jsbean Jul 29, 2019
5a7a64a
Merge early exits in ChoiceBox.init?(_: KeyedBox)
jsbean Jul 30, 2019
c000573
Tighten up SharedBox init callsite
jsbean Jul 30, 2019
d4bd9f4
Rename _converted -> converted
jsbean Jul 30, 2019
4a99e95
Beef up XMLChoiceCodingKey doc comment
jsbean Jul 30, 2019
7920b72
Rename local variable mySelf -> oldSelf
jsbean Jul 30, 2019
683cb34
Wrangle long preconditionFailure messages
jsbean Jul 30, 2019
7db9627
Reword Implement -> Implementing in doc comment
jsbean Jul 30, 2019
c213808
Throw errors instead of fatallyErroring
jsbean Jul 30, 2019
32195c5
Add brief description to README
jsbean Jul 30, 2019
8149ead
Keep README in tag-ological order
jsbean Jul 30, 2019
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
36 changes: 36 additions & 0 deletions Sources/XMLCoder/Auxiliaries/Box/ChoiceBox.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
//
// ChoiceBox.swift
// XMLCoder
//
// Created by James Bean on 7/18/19.
//

/// A `Box` which represents an element which is known to contain an XML choice element.
struct ChoiceBox {
var key: String = ""
var element: Box = NullBox()
}

extension ChoiceBox: Box {
var isNull: Bool {
return false
}

func xmlString() -> String? {
return nil
}
}

extension ChoiceBox: SimpleBox {}

extension ChoiceBox {
init?(_ keyedBox: KeyedBox) {
guard let firstKey = keyedBox.elements.keys.first else { return nil }
guard let firstElement = keyedBox.elements[firstKey].first else { return nil }
self.init(key: firstKey, element: firstElement)
}

init(_ singleKeyedBox: SingleKeyedBox) {
self.init(key: singleKeyedBox.key, element: singleKeyedBox.element)
}
}
24 changes: 24 additions & 0 deletions Sources/XMLCoder/Auxiliaries/Box/SingleKeyedBox.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// SingleKeyedBox.swift
// XMLCoder
//
// Created by James Bean on 7/15/19.
//

/// A `Box` which contains a single `key` and `element` pair. This is useful for disambiguating elements which could either represent
/// an element nested in a keyed or unkeyed container, or an choice between multiple known-typed values (implemented in Swift using
/// enums with associated values).
struct SingleKeyedBox: SimpleBox {
var key: String
var element: Box
}

extension SingleKeyedBox: Box {
var isNull: Bool {
return false
}

func xmlString() -> String? {
return nil
}
}
10 changes: 10 additions & 0 deletions Sources/XMLCoder/Auxiliaries/XMLChoiceCodingKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//
// XMLChoiceCodingKey.swift
// XMLCoder
//
// Created by Benjamin Wetherfield on 7/17/19.
//

/// An empty marker protocol that can be used in place of `CodingKey`. It must be used when conforming a union-type–like enum with
/// associated values to `Codable` when the encoded format is `XML`.
public protocol XMLChoiceCodingKey: CodingKey {}
22 changes: 16 additions & 6 deletions Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
Original file line number Diff line number Diff line change
@@ -63,9 +63,7 @@ struct XMLCoderElement: Equatable {
if elements.isEmpty, let value = value {
elements.append(StringBox(value), at: "value")
}
let keyedBox = KeyedBox(elements: elements, attributes: attributes)

return keyedBox
return KeyedBox(elements: elements, attributes: attributes)
}

func toXMLString(with header: XMLHeader? = nil,
@@ -245,9 +243,17 @@ struct XMLCoderElement: Equatable {

extension XMLCoderElement {
init(key: String, box: UnkeyedBox) {
self.init(key: key, elements: box.map {
XMLCoderElement(key: key, box: $0)
})
if let containsChoice = box as? [ChoiceBox] {
self.init(key: key, elements: containsChoice.map {
XMLCoderElement(key: $0.key, box: $0.element)
})
} else {
self.init(key: key, elements: box.map { XMLCoderElement(key: key, box: $0) })
}
}

init(key: String, box: ChoiceBox) {
self.init(key: key, elements: [XMLCoderElement(key: box.key, box: box.element)])
}

init(key: String, box: KeyedBox) {
@@ -302,10 +308,14 @@ extension XMLCoderElement {
self.init(key: key, box: sharedUnkeyedBox.unboxed)
case let sharedKeyedBox as SharedBox<KeyedBox>:
self.init(key: key, box: sharedKeyedBox.unboxed)
case let sharedChoiceBox as SharedBox<ChoiceBox>:
self.init(key: key, box: sharedChoiceBox.unboxed)
case let unkeyedBox as UnkeyedBox:
self.init(key: key, box: unkeyedBox)
case let keyedBox as KeyedBox:
self.init(key: key, box: keyedBox)
case let choiceBox as ChoiceBox:
self.init(key: key, box: choiceBox)
case let simpleBox as SimpleBox:
self.init(key: key, box: simpleBox)
case let box:
75 changes: 75 additions & 0 deletions Sources/XMLCoder/Decoder/XMLChoiceDecodingContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// XMLChoiceDecodingContainer.swift
// XMLCoder
//
// Created by James Bean on 7/18/19.
//

/// Container specialized for decoding XML choice elements.
struct XMLChoiceDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
typealias Key = K

// MARK: Properties

/// A reference to the decoder we're reading from.
private let decoder: XMLDecoderImplementation

/// A reference to the container we're reading from.
private let container: SharedBox<ChoiceBox>

/// The path of coding keys taken to get to this point in decoding.
public private(set) var codingPath: [CodingKey]

// MARK: - Initialization

/// Initializes `self` by referencing the given decoder and container.
init(referencing decoder: XMLDecoderImplementation, wrapping container: SharedBox<ChoiceBox>) {
self.decoder = decoder
container.withShared { $0.key = decoder.keyTransform($0.key) }
self.container = container
codingPath = decoder.codingPath
}

// MARK: - KeyedDecodingContainerProtocol Methods

public var allKeys: [Key] {
return container.withShared { [Key(stringValue: $0.key)!] }
}

public func contains(_ key: Key) -> Bool {
return container.withShared { $0.key == key.stringValue }
}

public func decodeNil(forKey key: Key) throws -> Bool {
return container.withShared { $0.element.isNull }
}

public func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
guard container.withShared({ $0.key == key.stringValue }), key is XMLChoiceCodingKey else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: type,
reality: container
)
}
return try decoder.unbox(container.withShared { $0.element })
}

public func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type, forKey key: Key
) throws -> KeyedDecodingContainer<NestedKey> {
fatalError("Choice elements cannot produce a nested container.")
}

public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
fatalError("Choice elements cannot produce a unkeyed nested container.")
}

public func superDecoder() throws -> Decoder {
fatalError("XMLChoiceDecodingContainer cannot produce a super decoder.")
}

public func superDecoder(forKey key: Key) throws -> Decoder {
fatalError("XMLChoiceDecodingContainer cannot produce a super decoder.")
}
}
71 changes: 65 additions & 6 deletions Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift
Original file line number Diff line number Diff line change
@@ -70,9 +70,15 @@ class XMLDecoderImplementation: Decoder {
return topContainer
}

public func container<Key>(
keyedBy keyType: Key.Type
) throws -> KeyedDecodingContainer<Key> {
public func container<Key>(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer<Key> {
if Key.self is XMLChoiceCodingKey.Type {
return try choiceContainer(keyedBy: keyType)
} else {
return try keyedContainer(keyedBy: keyType)
}
}

public func keyedContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
let topContainer = try self.topContainer()

switch topContainer {
@@ -118,6 +124,34 @@ class XMLDecoderImplementation: Decoder {
}
}

/// - Returns: A `KeyedDecodingContainer` for an XML choice element.
public func choiceContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
let topContainer = try self.topContainer()
let choiceBox: ChoiceBox?
switch topContainer {
case let choice as ChoiceBox:
choiceBox = choice
case let singleKeyed as SingleKeyedBox:
choiceBox = ChoiceBox(singleKeyed)
case let keyed as SharedBox<KeyedBox>:
choiceBox = ChoiceBox(keyed.withShared { $0 })
default:
choiceBox = nil
}
guard let box = choiceBox else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: topContainer
)
}
let container = XMLChoiceDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(box)
)
return KeyedDecodingContainer(container)
}

public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
let topContainer = try self.topContainer()

@@ -138,9 +172,15 @@ class XMLDecoderImplementation: Decoder {
case let unkeyed as SharedBox<UnkeyedBox>:
return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
case let keyed as SharedBox<KeyedBox>:
guard let firstKey = keyed.withShared({ $0.elements.keys.first }) else { fallthrough }

return XMLUnkeyedDecodingContainer(referencing: self, wrapping: SharedBox(keyed.withShared { $0.elements[firstKey] }))
return XMLUnkeyedDecodingContainer(
referencing: self,
wrapping: SharedBox(
keyed.withShared { $0.elements.map { key, box in
SingleKeyedBox(key: key, element: box)
}
}
)
)
default:
throw DecodingError.typeMismatch(
at: codingPath,
@@ -420,3 +460,22 @@ extension XMLDecoderImplementation {
return result
}
}

extension XMLDecoderImplementation {
var keyTransform: (String) -> String {
switch options.keyDecodingStrategy {
case .convertFromSnakeCase:
return XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase
case .convertFromCapitalized:
return XMLDecoder.KeyDecodingStrategy._convertFromCapitalized
case .convertFromKebabCase:
return XMLDecoder.KeyDecodingStrategy._convertFromKebabCase
case .useDefaultKeys:
return { key in key }
case let .custom(converter):
return { key in
converter(self.codingPath + [XMLKey(stringValue: key, intValue: nil)]).stringValue
}
}
}
}
44 changes: 4 additions & 40 deletions Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
@@ -34,47 +34,11 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
wrapping container: KeyedContainer
) {
self.decoder = decoder

func mapKeys(
_ container: KeyedContainer,
closure: (String) -> String
) -> KeyedContainer {
let attributes = container.withShared { keyedBox in
keyedBox.attributes.map { (closure($0), $1) }
}
let elements = container.withShared { keyedBox in
keyedBox.elements.map { (closure($0), $1) }
}
let keyedBox = KeyedBox(elements: elements, attributes: attributes)
return SharedBox(keyedBox)
}

switch decoder.options.keyDecodingStrategy {
case .useDefaultKeys:
self.container = container
case .convertFromSnakeCase:
// Convert the snake case keys in the container to camel case.
// If we hit a duplicate key after conversion, then we'll use the
// first one we saw. Effectively an undefined behavior with dictionaries.
self.container = mapKeys(container) { key in
XMLDecoder.KeyDecodingStrategy._convertFromSnakeCase(key)
}
case .convertFromKebabCase:
self.container = mapKeys(container) { key in
XMLDecoder.KeyDecodingStrategy._convertFromKebabCase(key)
}
case .convertFromCapitalized:
self.container = mapKeys(container) { key in
XMLDecoder.KeyDecodingStrategy._convertFromCapitalized(key)
}
case let .custom(converter):
self.container = mapKeys(container) { key in
let codingPath = decoder.codingPath + [
XMLKey(stringValue: key, intValue: nil),
]
return converter(codingPath).stringValue
}
container.withShared {
$0.elements = .init($0.elements.map { (decoder.keyTransform($0), $1) })
$0.attributes = .init($0.attributes.map { (decoder.keyTransform($0), $1) })
}
self.container = container
codingPath = decoder.codingPath
}

13 changes: 12 additions & 1 deletion Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
@@ -102,7 +102,18 @@ struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer {
unkeyedBox[self.currentIndex]
}

let value = try decode(decoder, box)
var value: T?
if let singleKeyed = box as? SingleKeyedBox {
do {
// Drill down to the element in the case of an nested unkeyed element
value = try decode(decoder, singleKeyed.element)
} catch {
// Specialize for choice elements
value = try decode(decoder, ChoiceBox(key: singleKeyed.key, element: singleKeyed.element))
}
} else {
value = try decode(decoder, box)
}

defer { currentIndex += 1 }

207 changes: 207 additions & 0 deletions Sources/XMLCoder/Encoder/XMLChoiceEncodingContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
//
// XMLChoiceEncodingContainer.swift
// XMLCoder
//
// Created by Benjamin Wetherfield on 7/17/19.
//

struct XMLChoiceEncodingContainer<K: CodingKey>: KeyedEncodingContainerProtocol {
typealias Key = K

// MARK: Properties

/// A reference to the encoder we're writing to.
private let encoder: XMLEncoderImplementation

/// A reference to the container we're writing to.
private var container: SharedBox<ChoiceBox>

/// The path of coding keys taken to get to this point in encoding.
public private(set) var codingPath: [CodingKey]

// MARK: - Initialization

/// Initializes `self` with the given references.
init(
referencing encoder: XMLEncoderImplementation,
codingPath: [CodingKey],
wrapping container: SharedBox<ChoiceBox>
) {
self.encoder = encoder
self.codingPath = codingPath
self.container = container
}

// MARK: - Coding Path Operations

private func _converted(_ key: CodingKey) -> CodingKey {
switch encoder.options.keyEncodingStrategy {
case .useDefaultKeys:
return key
case .convertToSnakeCase:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToSnakeCase(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case .convertToKebabCase:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToKebabCase(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case let .custom(converter):
return converter(codingPath + [key])
case .capitalized:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToCapitalized(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case .uppercased:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToUppercased(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
case .lowercased:
let newKeyString = XMLEncoder.KeyEncodingStrategy
._convertToLowercased(key.stringValue)
return XMLKey(stringValue: newKeyString, intValue: key.intValue)
}
}

// MARK: - KeyedEncodingContainerProtocol Methods

public mutating func encodeNil(forKey key: Key) throws {
container.withShared {
$0.key = _converted(key).stringValue
$0.element = NullBox()
}
}

public mutating func encode<T: Encodable>(
_ value: T,
forKey key: Key
) throws {
return try encode(value, forKey: key) { encoder, value in
try encoder.box(value)
}
}

private mutating func encode<T: Encodable>(
_ value: T,
forKey key: Key,
encode: (XMLEncoderImplementation, T) throws -> Box
) throws {
defer {
_ = self.encoder.nodeEncodings.removeLast()
self.encoder.codingPath.removeLast()
}
encoder.codingPath.append(key)
let nodeEncodings = encoder.options.nodeEncodingStrategy.nodeEncodings(
forType: T.self,
with: encoder
)
encoder.nodeEncodings.append(nodeEncodings)
let box = try encode(encoder, value)

let mySelf = self
let elementEncoder: (T, Key, Box) throws -> () = { _, key, box in
mySelf.container.withShared { container in
container.element = box
container.key = mySelf._converted(key).stringValue
}
}

defer {
self = mySelf
}

try elementEncoder(value, key, box)
}

public mutating func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
if NestedKey.self is XMLChoiceCodingKey.Type {
return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key)
} else {
return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key)
}
}

mutating func nestedKeyedContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
let sharedKeyed = SharedBox(KeyedBox())

self.container.withShared { container in
container.element = sharedKeyed
container.key = _converted(key).stringValue
}

codingPath.append(key)
defer { self.codingPath.removeLast() }

let container = XMLKeyedEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedKeyed
)
return KeyedEncodingContainer(container)
}

mutating func nestedChoiceContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
let sharedChoice = SharedBox(ChoiceBox())

self.container.withShared { container in
container.element = sharedChoice
container.key = _converted(key).stringValue
}

codingPath.append(key)
defer { self.codingPath.removeLast() }

let container = XMLChoiceEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedChoice
)
return KeyedEncodingContainer(container)
}

public mutating func nestedUnkeyedContainer(
forKey key: Key
) -> UnkeyedEncodingContainer {
let sharedUnkeyed = SharedBox(UnkeyedBox())

container.withShared { container in
container.element = sharedUnkeyed
container.key = _converted(key).stringValue
}

codingPath.append(key)
defer { self.codingPath.removeLast() }
return XMLUnkeyedEncodingContainer(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedUnkeyed
)
}

public mutating func superEncoder() -> Encoder {
return XMLReferencingEncoder(
referencing: encoder,
key: XMLKey.super,
convertedKey: _converted(XMLKey.super),
wrapping: container
)
}

public mutating func superEncoder(forKey key: Key) -> Encoder {
return XMLReferencingEncoder(
referencing: encoder,
key: key,
convertedKey: _converted(key),
wrapping: container
)
}
}
2 changes: 2 additions & 0 deletions Sources/XMLCoder/Encoder/XMLEncoder.swift
Original file line number Diff line number Diff line change
@@ -338,6 +338,8 @@ open class XMLEncoder {
elementOrNone = XMLCoderElement(key: rootKey, box: keyedBox)
} else if let unkeyedBox = topLevel as? UnkeyedBox {
elementOrNone = XMLCoderElement(key: rootKey, box: unkeyedBox)
} else if let choiceBox = topLevel as? ChoiceBox {
elementOrNone = XMLCoderElement(key: rootKey, box: choiceBox)
} else {
fatalError("Unrecognized top-level element of type: \(type(of: topLevel))")
}
25 changes: 25 additions & 0 deletions Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift
Original file line number Diff line number Diff line change
@@ -64,6 +64,14 @@ class XMLEncoderImplementation: Encoder {
// MARK: - Encoder Methods

public func container<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
if Key.self is XMLChoiceCodingKey.Type {
return choiceContainer(keyedBy: Key.self)
} else {
return keyedContainer(keyedBy: Key.self)
}
}

public func keyedContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
// If an existing keyed container was already requested, return that one.
let topContainer: SharedBox<KeyedBox>
if canEncodeNewValue {
@@ -81,6 +89,23 @@ class XMLEncoderImplementation: Encoder {
return KeyedEncodingContainer(container)
}

public func choiceContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
let topContainer: SharedBox<ChoiceBox>
if canEncodeNewValue {
// We haven't yet pushed a container at this level; do so here.
topContainer = storage.pushChoiceContainer()
} else {
guard let container = storage.lastContainer as? SharedBox<ChoiceBox> else {
preconditionFailure("Attempt to push new (single element) keyed encoding container when already previously encoded at this path.")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this string be split into multiple lines with """ to bring it closer to 80 characters, or at least 100?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this looks like a copy-the-style-as-it-is job. Do you want me to tidy up the other instances as well or just stick to the new code?

Copy link
Collaborator

@MaxDesiatov MaxDesiatov Jul 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes please, the overall cleanup is much appreciated!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a strict line length for the repo? The .swiftlint.yml declares violations at 150 and 200. I've been sticking to 100 out of habit, but I can constrain these to something else if it is so desired.

Copy link
Collaborator

@MaxDesiatov MaxDesiatov Jul 30, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been trying to get some balance between changing the whole repository and maintaining readability. 80 doesn't work well when indentation is 4 spaces instead of 2, but I decided to stick with 4 to preserve the changes history. You can clean up as much as you don't find tedious. I think last time I tried to tighten the limit in .swiftlint.yml (even to 120) it required too many code changes and reformatting XML snippets in the unit tests, which I just decided to postpone, but never got back to it. Maybe if there too many changes of this type, it can be made in a separate PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All seems fair to me! Probably worth waiting for the official swift-format to roll out to act on anything?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure, makes sense

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, I just touched the long preconditionFailure messages.

}

topContainer = container
}

let container = XMLChoiceEncodingContainer<Key>(referencing: self, codingPath: codingPath, wrapping: topContainer)
return KeyedEncodingContainer(container)
}

public func unkeyedContainer() -> UnkeyedEncodingContainer {
// If an existing unkeyed container was already requested, return that one.
let topContainer: SharedBox<UnkeyedBox>
6 changes: 6 additions & 0 deletions Sources/XMLCoder/Encoder/XMLEncodingStorage.swift
Original file line number Diff line number Diff line change
@@ -37,6 +37,12 @@ struct XMLEncodingStorage {
return container
}

mutating func pushChoiceContainer() -> SharedBox<ChoiceBox> {
let container = SharedBox(ChoiceBox())
containers.append(container)
return container
}

mutating func pushUnkeyedContainer() -> SharedBox<UnkeyedBox> {
let container = SharedBox(UnkeyedBox())
containers.append(container)
32 changes: 32 additions & 0 deletions Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift
Original file line number Diff line number Diff line change
@@ -141,6 +141,17 @@ struct XMLKeyedEncodingContainer<K: CodingKey>: KeyedEncodingContainerProtocol {
public mutating func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
if NestedKey.self is XMLChoiceCodingKey.Type {
return nestedChoiceContainer(keyedBy: NestedKey.self, forKey: key)
} else {
return nestedKeyedContainer(keyedBy: NestedKey.self, forKey: key)
}
}

mutating func nestedKeyedContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
let sharedKeyed = SharedBox(KeyedBox())

@@ -159,6 +170,27 @@ struct XMLKeyedEncodingContainer<K: CodingKey>: KeyedEncodingContainerProtocol {
return KeyedEncodingContainer(container)
}

mutating func nestedChoiceContainer<NestedKey>(
keyedBy _: NestedKey.Type,
forKey key: Key
) -> KeyedEncodingContainer<NestedKey> {
let sharedChoice = SharedBox(ChoiceBox())

self.container.withShared { container in
container.elements.append(sharedChoice, at: _converted(key).stringValue)
}

codingPath.append(key)
defer { self.codingPath.removeLast() }

let container = XMLChoiceEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedChoice
)
return KeyedEncodingContainer(container)
}

public mutating func nestedUnkeyedContainer(
forKey key: Key
) -> UnkeyedEncodingContainer {
25 changes: 25 additions & 0 deletions Sources/XMLCoder/Encoder/XMLReferencingEncoder.swift
Original file line number Diff line number Diff line change
@@ -24,6 +24,9 @@ class XMLReferencingEncoder: XMLEncoderImplementation {

/// Referencing a specific key in a keyed container.
case keyed(SharedBox<KeyedBox>, String)

/// Referencing a specific key in a keyed container.
case choice(SharedBox<ChoiceBox>, String)
}

// MARK: - Properties
@@ -71,6 +74,23 @@ class XMLReferencingEncoder: XMLEncoderImplementation {
codingPath.append(key)
}

init(
referencing encoder: XMLEncoderImplementation,
key: CodingKey,
convertedKey: CodingKey,
wrapping sharedKeyed: SharedBox<ChoiceBox>
) {
self.encoder = encoder
reference = .choice(sharedKeyed, convertedKey.stringValue)
super.init(
options: encoder.options,
nodeEncodings: encoder.nodeEncodings,
codingPath: encoder.codingPath
)

codingPath.append(key)
}

// MARK: - Coding Path Operations

override var canEncodeNewValue: Bool {
@@ -100,6 +120,11 @@ class XMLReferencingEncoder: XMLEncoderImplementation {
sharedKeyedBox.withShared { keyedBox in
keyedBox.elements.append(box, at: key)
}
case let .choice(sharedChoiceBox, key):
sharedChoiceBox.withShared { choiceBox in
choiceBox.element = box
choiceBox.key = key
}
}
}
}
25 changes: 25 additions & 0 deletions Sources/XMLCoder/Encoder/XMLUnkeyedEncodingContainer.swift
Original file line number Diff line number Diff line change
@@ -66,6 +66,14 @@ struct XMLUnkeyedEncodingContainer: UnkeyedEncodingContainer {
public mutating func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type
) -> KeyedEncodingContainer<NestedKey> {
if NestedKey.self is XMLChoiceCodingKey.Type {
return nestedChoiceContainer(keyedBy: NestedKey.self)
} else {
return nestedKeyedContainer(keyedBy: NestedKey.self)
}
}

public mutating func nestedKeyedContainer<NestedKey>(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
codingPath.append(XMLKey(index: count))
defer { self.codingPath.removeLast() }

@@ -82,6 +90,23 @@ struct XMLUnkeyedEncodingContainer: UnkeyedEncodingContainer {
return KeyedEncodingContainer(container)
}

public mutating func nestedChoiceContainer<NestedKey>(keyedBy _: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> {
codingPath.append(XMLKey(index: count))
defer { self.codingPath.removeLast() }

let sharedChoice = SharedBox(ChoiceBox())
self.container.withShared { container in
container.append(sharedChoice)
}

let container = XMLChoiceEncodingContainer<NestedKey>(
referencing: encoder,
codingPath: codingPath,
wrapping: sharedChoice
)
return KeyedEncodingContainer(container)
}

public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
codingPath.append(XMLKey(index: count))
defer { self.codingPath.removeLast() }
106 changes: 106 additions & 0 deletions Tests/XMLCoderTests/CompositeChoiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//
// CompositeChoiceTests.swift
// XMLCoderTests
//
// Created by James Bean on 7/15/19.
//

import XCTest
import XMLCoder

private struct IntWrapper: Codable, Equatable {
let wrapped: Int
}

private struct StringWrapper: Codable, Equatable {
let wrapped: String
}

private enum IntOrStringWrapper: Equatable {
case int(IntWrapper)
case string(StringWrapper)
}

extension IntOrStringWrapper: Codable {
enum CodingKeys: String, XMLChoiceCodingKey {
case int
case string
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self = .int(try container.decode(IntWrapper.self, forKey: .int))
} catch {
self = .string(try container.decode(StringWrapper.self, forKey: .string))
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .int(value):
try container.encode(value, forKey: .int)
case let .string(value):
try container.encode(value, forKey: .string)
}
}
}

class EnumAssociatedValueTestComposite: XCTestCase {
var encoder: XMLEncoder {
let encoder = XMLEncoder()
encoder.outputFormatting = [.prettyPrinted]
return encoder
}

private let simpleString = IntOrStringWrapper.string(StringWrapper(wrapped: "A Word About Woke Times"))

private let xmlSimpleString = """
<container>
<string>
<wrapped>A Word About Woke Times</wrapped>
</string>
</container>
"""

private let simpleArray: [IntOrStringWrapper] = [
.string(StringWrapper(wrapped: "A Word About Woke Times")),
.int(IntWrapper(wrapped: 9000)),
.string(StringWrapper(wrapped: "A Word About Woke Tomes")),
]

private let xmlSimpleArray = """
<container>
<string>
<wrapped>A Word About Woke Times</wrapped>
</string>
<int>
<wrapped>9000</wrapped>
</int>
<string>
<wrapped>A Word About Woke Tomes</wrapped>
</string>
</container>
"""

func testDecodeIntOrStringWrapper() throws {
let decoded = try XMLDecoder().decode(IntOrStringWrapper.self, from: xmlSimpleString.data(using: .utf8)!)
XCTAssertEqual(decoded, simpleString)
}

func testEncodeIntOrStringWrapper() throws {
let encoded = try encoder.encode(simpleString, withRootKey: "container")
XCTAssertEqual(String(data: encoded, encoding: .utf8), xmlSimpleString)
}

func testDecodeArrayOfIntOrStringWrappers() throws {
let decoded = try XMLDecoder().decode([IntOrStringWrapper].self, from: xmlSimpleArray.data(using: .utf8)!)
XCTAssertEqual(decoded, simpleArray)
}

func testEncodeArrayOfIntOrStringWrappers() throws {
let encoded = try encoder.encode(simpleArray, withRootKey: "container")
XCTAssertEqual(String(data: encoded, encoding: .utf8), xmlSimpleArray)
}
}
3 changes: 1 addition & 2 deletions Tests/XMLCoderTests/Minimal/BoxTreeTests.swift
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ import XCTest
@testable import XMLCoder

class BoxTreeTests: XCTestCase {
func testNestedValues() {
func testNestedValues() throws {
let e1 = XMLCoderElement(
key: "foo",
value: "456",
@@ -31,7 +31,6 @@ class BoxTreeTests: XCTestCase {

let boxTree = root.transformToBoxTree()
let foo = boxTree.elements["foo"]

XCTAssertEqual(foo.count, 2)
}
}
144 changes: 144 additions & 0 deletions Tests/XMLCoderTests/NestedAttributeChoiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
//
// NestedAttributeChoiceTests.swift
// XMLCoderTests
//
// Created by Benjamin Wetherfield on 7/23/19.
//

import XCTest
import XMLCoder

private struct Container: Equatable {
let paragraphs: [Paragraph]
}

private struct Paragraph: Equatable {
let entries: [Entry]
}

private enum Entry: Equatable {
case run(Run)
case properties(Properties)
case br(Break)
}

private struct Run: Codable, Equatable, DynamicNodeEncoding {
let id: Int
let text: String

static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.id:
return .attribute
default:
return .element
}
}
}

private struct Properties: Codable, Equatable, DynamicNodeEncoding {
let id: Int
let title: String

static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
return .attribute
}
}

private struct Break: Codable, Equatable {}

extension Container: Codable {
enum CodingKeys: String, CodingKey {
case paragraphs = "p"
}
}

extension Paragraph: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
entries = try container.decode([Entry].self)
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(entries)
}
}

extension Entry: Codable {
private enum CodingKeys: String, XMLChoiceCodingKey {
case run, properties, br
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self = .run(try container.decode(Run.self, forKey: .run))
} catch {
do {
self = .properties(try container.decode(Properties.self, forKey: .properties))
} catch {
self = .br(try container.decode(Break.self, forKey: .br))
}
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .run(value):
try container.encode(value, forKey: .run)
case let .properties(value):
try container.encode(value, forKey: .properties)
case let .br(value):
try container.encode(value, forKey: .br)
}
}
}

class NestedAttributeChoiceTests: XCTestCase {
private var encoder: XMLEncoder {
let encoder = XMLEncoder()
encoder.outputFormatting = [.prettyPrinted]
return encoder
}

func testNestedEnumsEncoding() throws {
let xml = """
<container>
<p>
<br />
<run id="1518">
<text>I am answering it again.</text>
</run>
<properties id="431" title="A Word About Wake Times" />
</p>
<p>
<run id="1519">
<text>I am answering it again.</text>
</run>
<br />
</p>
</container>
"""
let value = Container(
paragraphs: [
Paragraph(
entries: [
.br(Break()),
.run(Run(id: 1518, text: "I am answering it again.")),
.properties(Properties(id: 431, title: "A Word About Wake Times")),
]
),
Paragraph(
entries: [
.run(Run(id: 1519, text: "I am answering it again.")),
.br(Break()),
]
),
]
)
let encoded = try encoder.encode(value, withRootKey: "container")
XCTAssertEqual(String(data: encoded, encoding: .utf8), xml)
}
}
250 changes: 250 additions & 0 deletions Tests/XMLCoderTests/NestedChoiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
//
// NestedChoiceTests.swift
// XMLCoderTests
//
// Created by James Bean on 7/15/19.
//

import XCTest
import XMLCoder

private struct Container: Equatable {
let paragraphs: [Paragraph]
}

private struct Paragraph: Equatable {
let entries: [Entry]
}

private enum Entry: Equatable {
case run(Run)
case properties(Properties)
case br(Break)
}

private struct Run: Codable, Equatable {
let id: Int
let text: String
}

private struct Properties: Codable, Equatable {
let id: Int
let title: String
}

private struct Break: Codable, Equatable {}

extension Container: Codable {
enum CodingKeys: String, CodingKey {
case paragraphs = "p"
}
}

extension Paragraph: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
entries = try container.decode([Entry].self)
}

func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(entries)
}
}

extension Entry: Codable {
private enum CodingKeys: String, XMLChoiceCodingKey {
case run, properties, br
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self = .run(try container.decode(Run.self, forKey: .run))
} catch {
do {
self = .properties(try container.decode(Properties.self, forKey: .properties))
} catch {
self = .br(try container.decode(Break.self, forKey: .br))
}
}
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .run(value):
try container.encode(value, forKey: .run)
case let .properties(value):
try container.encode(value, forKey: .properties)
case let .br(value):
try container.encode(value, forKey: .br)
}
}
}

class NestedChoiceTests: XCTestCase {
func testBreakDecoding() throws {
let xml = "<br></br>"
let result = try XMLDecoder().decode(Break.self, from: xml.data(using: .utf8)!)
let expected = Break()
XCTAssertEqual(result, expected)
}

func testPropertiesDecoding() throws {
let xml = """
<properties>
<id>431</id>
<title>A Word About Wake Times</title>
</properties>
"""
let result = try XMLDecoder().decode(Properties.self, from: xml.data(using: .utf8)!)
let expected = Properties(id: 431, title: "A Word About Wake Times")
XCTAssertEqual(result, expected)
}

func testPropertiesAsEntryDecoding() throws {
let xml = """
<entry>
<properties>
<id>431</id>
<title>A Word About Wake Times</title>
</properties>
</entry>
"""
let result = try XMLDecoder().decode(Entry.self, from: xml.data(using: .utf8)!)
let expected: Entry = .properties(Properties(id: 431, title: "A Word About Wake Times"))
XCTAssertEqual(result, expected)
}

func testRunDecoding() throws {
let xml = """
<run>
<id>1518</id>
<text>I am answering it again.</text>
</run>
"""
let result = try XMLDecoder().decode(Run.self, from: xml.data(using: .utf8)!)
let expected = Run(id: 1518, text: "I am answering it again.")
XCTAssertEqual(result, expected)
}

func testRunAsEntryDecoding() throws {
let xml = """
<entry>
<run>
<id>1518</id>
<text>I am answering it again.</text>
</run>
</entry>
"""
let result = try XMLDecoder().decode(Entry.self, from: xml.data(using: .utf8)!)
let expected = Entry.run(Run(id: 1518, text: "I am answering it again."))
XCTAssertEqual(result, expected)
}

func testEntriesDecoding() throws {
let xml = """
<entries>
<run>
<id>1518</id>
<text>I am answering it again.</text>
</run>
<properties>
<id>431</id>
<title>A Word About Wake Times</title>
</properties>
</entries>
"""
let result = try XMLDecoder().decode([Entry].self, from: xml.data(using: .utf8)!)
let expected: [Entry] = [
.run(Run(id: 1518, text: "I am answering it again.")),
.properties(Properties(id: 431, title: "A Word About Wake Times")),
]
XCTAssertEqual(result, expected)
}

func testParagraphDecoding() throws {
let xml = """
<p>
<run>
<id>1518</id>
<text>I am answering it again.</text>
</run>
<properties>
<id>431</id>
<title>A Word About Wake Times</title>
</properties>
</p>
"""
let result = try XMLDecoder().decode(Paragraph.self, from: xml.data(using: .utf8)!)
let expected = Paragraph(
entries: [
.run(Run(id: 1518, text: "I am answering it again.")),
.properties(Properties(id: 431, title: "A Word About Wake Times")),
]
)
XCTAssertEqual(result, expected)
}

func testNestedEnums() throws {
let xml = """
<container>
<p>
<run>
<id>1518</id>
<text>I am answering it again.</text>
</run>
<properties>
<id>431</id>
<title>A Word About Wake Times</title>
</properties>
</p>
<p>
<run>
<id>1519</id>
<text>I am answering it again.</text>
</run>
</p>
</container>
"""
let result = try XMLDecoder().decode(Container.self, from: xml.data(using: .utf8)!)
let expected = Container(
paragraphs: [
Paragraph(
entries: [
.run(Run(id: 1518, text: "I am answering it again.")),
.properties(Properties(id: 431, title: "A Word About Wake Times")),
]
),
Paragraph(
entries: [
.run(Run(id: 1519, text: "I am answering it again.")),
]
),
]
)
XCTAssertEqual(result, expected)
}

func testNestedEnumsRoundTrip() throws {
let original = Container(
paragraphs: [
Paragraph(
entries: [
.run(Run(id: 1518, text: "I am answering it again.")),
.properties(Properties(id: 431, title: "A Word About Wake Times")),
]
),
Paragraph(
entries: [
.run(Run(id: 1519, text: "I am answering it again.")),
]
),
]
)
let encoded = try XMLEncoder().encode(original, withRootKey: "container")
let decoded = try XMLDecoder().decode(Container.self, from: encoded)
XCTAssertEqual(decoded, original)
}
}
146 changes: 80 additions & 66 deletions Tests/XMLCoderTests/NestingTests.swift
Original file line number Diff line number Diff line change
@@ -25,108 +25,122 @@ final class NestingTests: XCTestCase {
[1, 2, 3],
]

let xmlUnkeyedWithinUnkeyed =
"""
<element>
<element>
<element>1</element>
<element>2</element>
<element>3</element>
</element>
<element>
<element>1</element>
<element>2</element>
<element>3</element>
</element>
</element>
"""

let unkeyedWithinKeyed: [String: [Int]] = [
"first": [1, 2, 3],
"second": [1, 2, 3],
]

let xmlUnkeyedWithinKeyed =
"""
<element>
<first>1</first>
<first>2</first>
<first>3</first>
<second>1</second>
<second>2</second>
<second>3</second>
</element>
"""

let keyedWithinUnkeyed: [[String: Int]] = [
["first": 1],
["second": 2],
]

let xmlKeyedWithinUnkeyed =
"""
<element>
<element>
<first>1</first>
</element>
<element>
<second>2</second>
</element>
</element>
"""

let keyedWithinKeyed: [String: [String: Int]] = [
"first": ["a": 1, "b": 2],
"second": ["c": 3, "d": 4],
]

let xmlKeyedWithinKeyed =
"""
<element>
<first>
<b>2</b>
<a>1</a>
</first>
<second>
<c>3</c>
<d>4</d>
</second>
</element>
"""

func testEncodeUnkeyedWithinUnkeyed() throws {
XCTAssertNoThrow(try encoder.encode(unkeyedWithinUnkeyed, withRootKey: "element"))
let encoded = try encoder.encode(unkeyedWithinUnkeyed, withRootKey: "element")
XCTAssertEqual(String(data: encoded, encoding: .utf8), xmlUnkeyedWithinUnkeyed)
}

func testEncodeUnkeyedWithinKeyed() throws {
XCTAssertNoThrow(try encoder.encode(unkeyedWithinKeyed, withRootKey: "element"))
let encoded = try encoder.encode(unkeyedWithinKeyed, withRootKey: "element")
let decoded = try decoder.decode([String: [Int]].self, from: encoded)
XCTAssertEqual(decoded, unkeyedWithinKeyed)
}

func testEncodeKeyedWithinUnkeyed() throws {
XCTAssertNoThrow(try encoder.encode(keyedWithinUnkeyed, withRootKey: "element"))
let encoded = try encoder.encode(keyedWithinUnkeyed, withRootKey: "element")
XCTAssertEqual(String(data: encoded, encoding: .utf8), xmlKeyedWithinUnkeyed)
}

func testEncodeKeyedWithinKeyed() throws {
XCTAssertNoThrow(try encoder.encode(keyedWithinKeyed, withRootKey: "element"))
let encoded = try encoder.encode(keyedWithinKeyed, withRootKey: "element")
let decoded = try decoder.decode([String: [String: Int]].self, from: encoded)
XCTAssertEqual(decoded, keyedWithinKeyed)
}

func testDecodeUnkeyedWithinUnkeyed() throws {
let xml =
"""
<element>
<element>
<element>1</element>
<element>2</element>
<element>3</element>
</element>
<element>
<element>1</element>
<element>2</element>
<element>3</element>
</element>
</element>
"""
let encoded = xml.data(using: .utf8)!

XCTAssertNoThrow(try decoder.decode(type(of: unkeyedWithinUnkeyed), from: encoded))
let encoded = xmlUnkeyedWithinUnkeyed.data(using: .utf8)!
let expected = unkeyedWithinUnkeyed
let decoded = try decoder.decode([[Int]].self, from: encoded)
XCTAssertEqual(decoded, expected)
}

func testDecodeUnkeyedWithinKeyed() throws {
let xml =
"""
<element>
<first>1</first>
<first>2</first>
<first>3</first>
<second>1</second>
<second>2</second>
<second>3</second>
</element>
"""
let encoded = xml.data(using: .utf8)!

XCTAssertNoThrow(try decoder.decode(type(of: unkeyedWithinKeyed), from: encoded))
let encoded = xmlUnkeyedWithinKeyed.data(using: .utf8)!
let expected = unkeyedWithinKeyed
let decoded = try decoder.decode([String: [Int]].self, from: encoded)
XCTAssertEqual(decoded, expected)
}

func testDecodeKeyedWithinUnkeyed() throws {
let xml =
"""
<element>
<element>
<first>1</first>
</element>
<element>
<second>2</second>
</element>
</element>
"""
let encoded = xml.data(using: .utf8)!

XCTAssertNoThrow(try decoder.decode(type(of: keyedWithinUnkeyed), from: encoded))
let encoded = xmlKeyedWithinUnkeyed.data(using: .utf8)!
let expected = keyedWithinUnkeyed
let decoded = try decoder.decode([[String: Int]].self, from: encoded)
XCTAssertEqual(decoded, expected)
}

func testDecodeKeyedWithinKeyed() throws {
let xml =
"""
<element>
<first>
<b>2</b>
<a>1</a>
</first>
<second>
<c>3</c>
<d>4</d>
</second>
</element>
"""
let encoded = xml.data(using: .utf8)!

XCTAssertNoThrow(try decoder.decode(type(of: keyedWithinKeyed), from: encoded))
let encoded = xmlKeyedWithinKeyed.data(using: .utf8)!
let expected = keyedWithinKeyed
let decoded = try decoder.decode([String: [String: Int]].self, from: encoded)
XCTAssertEqual(decoded, expected)
}
}
118 changes: 118 additions & 0 deletions Tests/XMLCoderTests/SimpleChoiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// SimpleChoiceTests.swift
// XMLCoderTests
//
// Created by James Bean on 7/15/19.
//

import XCTest
import XMLCoder

private enum IntOrString: Equatable {
case int(Int)
case string(String)
}

extension IntOrString: Codable {
enum CodingKeys: String, XMLChoiceCodingKey {
case int
case string
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case let .int(value):
try container.encode(value, forKey: .int)
case let .string(value):
try container.encode(value, forKey: .string)
}
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self = .int(try container.decode(Int.self, forKey: .int))
} catch {
self = .string(try container.decode(String.self, forKey: .string))
}
}
}

class SimpleChoiceTests: XCTestCase {
func testIntOrStringIntDecoding() throws {
let xml = """
<container>
<int>42</int>
</container>
"""
let result = try XMLDecoder().decode(IntOrString.self, from: xml.data(using: .utf8)!)
let expected = IntOrString.int(42)
XCTAssertEqual(result, expected)
}

func testIntOrStringStringDecoding() throws {
let xml = """
<container>
<string>forty-two</string>"
</container>
"""
let result = try XMLDecoder().decode(IntOrString.self, from: xml.data(using: .utf8)!)
let expected = IntOrString.string("forty-two")
XCTAssertEqual(result, expected)
}

func testIntOrStringArrayDecoding() throws {
let xml = """
<container>
<int>1</int>
<string>two</string>
<string>three</string>
<int>4</int>
<int>5</int>
</container>
"""
let result = try XMLDecoder().decode([IntOrString].self, from: xml.data(using: .utf8)!)
let expected: [IntOrString] = [
.int(1),
.string("two"),
.string("three"),
.int(4),
.int(5),
]
XCTAssertEqual(result, expected)
}

func testIntOrStringRoundTrip() throws {
let original = IntOrString.int(5)
let encoded = try XMLEncoder().encode(original, withRootKey: "container")
let decoded = try XMLDecoder().decode(IntOrString.self, from: encoded)
XCTAssertEqual(original, decoded)
}

func testIntOrStringArrayRoundTrip() throws {
let original: [IntOrString] = [
.int(1),
.string("two"),
.string("three"),
.int(4),
.int(5),
]
let encoded = try XMLEncoder().encode(original, withRootKey: "container")
let decoded = try XMLDecoder().decode([IntOrString].self, from: encoded)
XCTAssertEqual(original, decoded)
}

func testIntOrStringDoubleArrayRoundTrip() throws {
let original: [[IntOrString]] = [[
.int(1),
.string("two"),
.string("three"),
.int(4),
.int(5),
]]
let encoded = try XMLEncoder().encode(original, withRootKey: "container")
let decoded = try XMLDecoder().decode([[IntOrString]].self, from: encoded)
XCTAssertEqual(original, decoded)
}
}
1,212 changes: 625 additions & 587 deletions XMLCoder.xcodeproj/project.pbxproj

Large diffs are not rendered by default.