Skip to content

Commit

Permalink
Mixed choice/non-choice decoding (#155)
Browse files Browse the repository at this point in the history
Mirrors #154 

Fixes bugs 
1) Decoding multiple choice elements in the same Keyed Container.
2) Decoding choice elements after decoding regular keyed elements in the same container.

Case 1 refers to decoding implementations of the form: 
```swift
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        otherValue = try container.decode(String.self, forKey: .otherValue)
        intOrString = try IntOrString(from: decoder)
    }
```
where `IntOrString` is a choice coding element

`IntOrString` defined as follows:

```swift 
enum IntOrString {
    case int(Int)
    case string(String)
}

extension IntOrString: Decodable {
    enum CodingKeys: String, CodingKey {
        case int
        case string
    }

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

extension IntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `IntOrString` is a choice element
```

Case 2 refers to decoding implementations of the following form:

```swift
    init(from decoder: Decoder) throws {
        self.first = try IntOrString(from: decoder)
        self.second = try AlternateIntOrString(from: decoder)
    }
```

Where both `first: IntOrString` and `second: AlternateIntOrString` are choice elements.

`AlternateIntOrString` defined as follows:

```swift
private enum AlternateIntOrString {
    case alternateInt(Int)
    case alternateString(String)
}

extension AlternateIntOrString: Decodable {
    enum CodingKeys: String, CodingKey {
        case alternateInt = "alternate-int"
        case alternateString = "alternate-string"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if container.contains(.alternateInt) {
            self = .alternateInt(try container.decode(Int.self, forKey: .alternateInt))
        } else {
            self = .alternateString(try container.decode(String.self, forKey: .alternateString))
        }
    }
}

extension AlternateIntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `AlternateIntOrString` is a choice element
```

* Add tests
* Fix failing tests
  • Loading branch information
bwetherfield authored and MaxDesiatov committed Dec 1, 2019
1 parent deb2ab2 commit c47aa6a
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 77 deletions.
129 changes: 61 additions & 68 deletions Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift
Expand Up @@ -71,16 +71,59 @@ class XMLDecoderImplementation: Decoder {
}

public func container<Key>(keyedBy keyType: Key.Type) throws -> KeyedDecodingContainer<Key> {
if let keyed = try self.topContainer() as? SharedBox<KeyedBox> {
return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
referencing: self,
wrapping: keyed
))
}
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> {
public func unkeyedContainer() throws -> UnkeyedDecodingContainer {
let topContainer = try self.topContainer()

guard !topContainer.isNull else {
throw DecodingError.valueNotFound(
UnkeyedDecodingContainer.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription:
"""
Cannot get unkeyed decoding container -- found null box instead.
"""
)
)
}

switch topContainer {
case let unkeyed as SharedBox<UnkeyedBox>:
return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
case let keyed as SharedBox<KeyedBox>:
return XMLUnkeyedDecodingContainer(
referencing: self,
wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleKeyedBox.init) })
)
default:
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [Any].self,
reality: topContainer
)
}
}

public func singleValueContainer() throws -> SingleValueDecodingContainer {
return self
}

private func keyedContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
let topContainer = try self.topContainer()
let keyedBox: KeyedBox
switch topContainer {
case _ where topContainer.isNull:
throw DecodingError.valueNotFound(
Expand All @@ -94,57 +137,44 @@ class XMLDecoderImplementation: Decoder {
)
)
case let string as StringBox:
return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(KeyedBox(
elements: KeyedStorage([("", string)]),
attributes: KeyedStorage()
))
))
keyedBox = KeyedBox(
elements: KeyedStorage([("", string)]),
attributes: KeyedStorage()
)
case let containsEmpty as SingleKeyedBox where containsEmpty.element is NullBox:
return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
referencing: self, wrapping: SharedBox(KeyedBox(
elements: KeyedStorage([("", StringBox(""))]), attributes: KeyedStorage()
))
))
case let keyed as SharedBox<KeyedBox>:
return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
referencing: self,
wrapping: keyed
))
keyedBox = KeyedBox(
elements: KeyedStorage([("", StringBox(""))]),
attributes: KeyedStorage()
)
case let unkeyed as SharedBox<UnkeyedBox>:
guard let keyed = unkeyed.withShared({ $0.first }) as? KeyedBox else {
fallthrough
}

return KeyedDecodingContainer(XMLKeyedDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(keyed)
))
keyedBox = keyed
default:
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: topContainer
)
}
let container = XMLKeyedDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(keyedBox)
)
return KeyedDecodingContainer(container)
}

/// - Returns: A `KeyedDecodingContainer` for an XML choice element.
public func choiceContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
private func choiceContainer<Key>(keyedBy _: Key.Type) throws -> KeyedDecodingContainer<Key> {
let topContainer = try self.topContainer()
let choiceBox: ChoiceBox?
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,
Expand All @@ -153,47 +183,10 @@ class XMLDecoderImplementation: Decoder {
}
let container = XMLChoiceDecodingContainer<Key>(
referencing: self,
wrapping: SharedBox(box)
wrapping: SharedBox(choiceBox)
)
return KeyedDecodingContainer(container)
}

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

guard !topContainer.isNull else {
throw DecodingError.valueNotFound(
UnkeyedDecodingContainer.self,
DecodingError.Context(
codingPath: codingPath,
debugDescription:
"""
Cannot get unkeyed decoding container -- found null box instead.
"""
)
)
}

switch topContainer {
case let unkeyed as SharedBox<UnkeyedBox>:
return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed)
case let keyed as SharedBox<KeyedBox>:
return XMLUnkeyedDecodingContainer(
referencing: self,
wrapping: SharedBox(keyed.withShared { $0.elements.map(SingleKeyedBox.init) })
)
default:
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [Any].self,
reality: topContainer
)
}
}

public func singleValueContainer() throws -> SingleValueDecodingContainer {
return self
}
}

// MARK: - Concrete Value Representations
Expand Down
109 changes: 100 additions & 9 deletions Tests/XMLCoderTests/MixedChoiceAndNonChoiceTests.swift
Expand Up @@ -8,12 +8,45 @@
import XCTest
import XMLCoder

private enum AlternateIntOrString: Equatable {
case alternateInt(Int)
case alternateString(String)
}

extension AlternateIntOrString: Codable {
enum CodingKeys: String, CodingKey {
case alternateInt = "alternate-int"
case alternateString = "alternate-string"
}

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

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if container.contains(.alternateInt) {
self = .alternateInt(try container.decode(Int.self, forKey: .alternateInt))
} else {
self = .alternateString(try container.decode(String.self, forKey: .alternateString))
}
}
}

extension AlternateIntOrString.CodingKeys: XMLChoiceCodingKey {}

private struct MixedIntOrStringFirst: Equatable {
let intOrString: IntOrString
let otherValue: String
}

extension MixedIntOrStringFirst: Encodable {
extension MixedIntOrStringFirst: Codable {
enum CodingKeys: String, CodingKey {
case otherValue = "other-value"
}
Expand All @@ -23,14 +56,20 @@ extension MixedIntOrStringFirst: Encodable {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(otherValue, forKey: .otherValue)
}

init(from decoder: Decoder) throws {
intOrString = try IntOrString(from: decoder)
let container = try decoder.container(keyedBy: CodingKeys.self)
otherValue = try container.decode(String.self, forKey: .otherValue)
}
}

private struct MixedOtherFirst: Equatable {
let intOrString: IntOrString
let otherValue: String
}

extension MixedOtherFirst: Encodable {
extension MixedOtherFirst: Codable {
enum CodingKeys: String, CodingKey {
case otherValue = "other-value"
}
Expand All @@ -40,15 +79,21 @@ extension MixedOtherFirst: Encodable {
try container.encode(otherValue, forKey: .otherValue)
try intOrString.encode(to: encoder)
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
otherValue = try container.decode(String.self, forKey: .otherValue)
intOrString = try IntOrString(from: decoder)
}
}

private struct MixedEitherSide {
private struct MixedEitherSide: Equatable {
let leading: String
let intOrString: IntOrString
let trailing: String
}

extension MixedEitherSide: Encodable {
extension MixedEitherSide: Codable {
enum CodingKeys: String, CodingKey {
case leading
case trailing
Expand All @@ -60,18 +105,30 @@ extension MixedEitherSide: Encodable {
try intOrString.encode(to: encoder)
try container.encode(trailing, forKey: .trailing)
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
leading = try container.decode(String.self, forKey: .leading)
intOrString = try IntOrString(from: decoder)
trailing = try container.decode(String.self, forKey: .trailing)
}
}

private struct TwoChoiceElements {
private struct TwoChoiceElements: Equatable {
let first: IntOrString
let second: IntOrString
let second: AlternateIntOrString
}

extension TwoChoiceElements: Encodable {
extension TwoChoiceElements: Codable {
func encode(to encoder: Encoder) throws {
try first.encode(to: encoder)
try second.encode(to: encoder)
}

init(from decoder: Decoder) throws {
first = try IntOrString(from: decoder)
second = try AlternateIntOrString(from: decoder)
}
}

class MixedChoiceAndNonChoiceTests: XCTestCase {
Expand All @@ -82,13 +139,29 @@ class MixedChoiceAndNonChoiceTests: XCTestCase {
XCTAssertEqual(String(data: firstEncoded, encoding: .utf8), firstExpectedXML)
}

func testMixedChoiceFirstDecode() throws {
let xmlString = "<container><int>4</int><other-value>other</other-value></container>"
let xmlData = xmlString.data(using: .utf8)!
let decoded = try XMLDecoder().decode(MixedIntOrStringFirst.self, from: xmlData)
let expected = MixedIntOrStringFirst(intOrString: .int(4), otherValue: "other")
XCTAssertEqual(decoded, expected)
}

func testMixedChoiceSecondEncode() throws {
let second = MixedOtherFirst(intOrString: .int(4), otherValue: "other")
let secondEncoded = try XMLEncoder().encode(second, withRootKey: "container")
let secondExpectedXML = "<container><other-value>other</other-value><int>4</int></container>"
XCTAssertEqual(String(data: secondEncoded, encoding: .utf8), secondExpectedXML)
}

func testMixedChoiceSecondDecode() throws {
let xmlString = "<container><other-value>other</other-value><int>4</int></container>"
let xmlData = xmlString.data(using: .utf8)!
let decoded = try XMLDecoder().decode(MixedOtherFirst.self, from: xmlData)
let expected = MixedOtherFirst(intOrString: .int(4), otherValue: "other")
XCTAssertEqual(decoded, expected)
}

func testMixedChoiceFlankedEncode() throws {
let flanked = MixedEitherSide(leading: "first", intOrString: .string("then"), trailing: "second")
let flankedEncoded = try XMLEncoder().encode(flanked, withRootKey: "container")
Expand All @@ -98,10 +171,28 @@ class MixedChoiceAndNonChoiceTests: XCTestCase {
XCTAssertEqual(String(data: flankedEncoded, encoding: .utf8), flankedExpectedXML)
}

func testMixedChoiceFlankedDecode() throws {
let xmlString = """
<container><leading>first</leading><string>then</string><trailing>second</trailing></container>
"""
let xmlData = xmlString.data(using: .utf8)!
let decoded = try XMLDecoder().decode(MixedEitherSide.self, from: xmlData)
let expected = MixedEitherSide(leading: "first", intOrString: .string("then"), trailing: "second")
XCTAssertEqual(decoded, expected)
}

func testTwoChoiceElementsEncode() throws {
let twoChoiceElements = TwoChoiceElements(first: .int(1), second: .string("one"))
let twoChoiceElements = TwoChoiceElements(first: .int(1), second: .alternateString("one"))
let encoded = try XMLEncoder().encode(twoChoiceElements, withRootKey: "container")
let expectedXML = "<container><int>1</int><string>one</string></container>"
let expectedXML = "<container><int>1</int><alternate-string>one</alternate-string></container>"
XCTAssertEqual(String(data: encoded, encoding: .utf8), expectedXML)
}

func testTwoChoiceElementsDecode() throws {
let xmlString = "<container><int>1</int><alternate-string>one</alternate-string></container>"
let xmlData = xmlString.data(using: .utf8)!
let decoded = try XMLDecoder().decode(TwoChoiceElements.self, from: xmlData)
let expected = TwoChoiceElements(first: .int(1), second: .alternateString("one"))
XCTAssertEqual(decoded, expected)
}
}
4 changes: 4 additions & 0 deletions Tests/XMLCoderTests/XCTestManifests.swift
Expand Up @@ -419,9 +419,13 @@ extension MixedChoiceAndNonChoiceTests {
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__MixedChoiceAndNonChoiceTests = [
("testMixedChoiceFirstDecode", testMixedChoiceFirstDecode),
("testMixedChoiceFirstEncode", testMixedChoiceFirstEncode),
("testMixedChoiceFlankedDecode", testMixedChoiceFlankedDecode),
("testMixedChoiceFlankedEncode", testMixedChoiceFlankedEncode),
("testMixedChoiceSecondDecode", testMixedChoiceSecondDecode),
("testMixedChoiceSecondEncode", testMixedChoiceSecondEncode),
("testTwoChoiceElementsDecode", testTwoChoiceElementsDecode),
("testTwoChoiceElementsEncode", testTwoChoiceElementsEncode),
]
}
Expand Down

0 comments on commit c47aa6a

Please sign in to comment.