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

Mixed choice/non-choice decoding #155

Merged
merged 2 commits into from Dec 1, 2019
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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