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 encoding #154

Merged
merged 15 commits into from Nov 27, 2019
96 changes: 51 additions & 45 deletions Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift
Expand Up @@ -64,58 +64,16 @@ class XMLEncoderImplementation: Encoder {
// MARK: - Encoder Methods

public func container<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
guard canEncodeNewValue else {
return mergeWithExistingKeyedContainer(keyedBy: Key.self)
}
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 {
// We haven't yet pushed a container at this level; do so here.
topContainer = storage.pushKeyedContainer()
} else {
guard let container = storage.lastContainer as? SharedBox<KeyedBox> else {
preconditionFailure(
"""
Attempt to push new keyed encoding container when already previously encoded \
at this path.
"""
)
}

topContainer = container
}

let container = XMLKeyedEncodingContainer<Key>(referencing: self, codingPath: codingPath, wrapping: topContainer)
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.
"""
)
}

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>
Expand All @@ -141,6 +99,54 @@ class XMLEncoderImplementation: Encoder {
public func singleValueContainer() -> SingleValueEncodingContainer {
return self
}

private func keyedContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
let container = XMLKeyedEncodingContainer<Key>(
referencing: self,
codingPath: codingPath,
wrapping: storage.pushKeyedContainer()
)
return KeyedEncodingContainer(container)
}

private func choiceContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
let container = XMLChoiceEncodingContainer<Key>(
referencing: self,
codingPath: codingPath,
wrapping: storage.pushChoiceContainer()
)
return KeyedEncodingContainer(container)
}

private func mergeWithExistingKeyedContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
switch storage.lastContainer {
case let keyed as SharedBox<KeyedBox>:
let container = XMLKeyedEncodingContainer<Key>(
referencing: self,
codingPath: codingPath,
wrapping: keyed
)
return KeyedEncodingContainer(container)
case let choice as SharedBox<ChoiceBox>:
_ = storage.popContainer()
let keyed = KeyedBox(
elements: KeyedBox.Elements([choice.withShared { ($0.key, $0.element) }]),
attributes: []
)
let container = XMLKeyedEncodingContainer<Key>(
referencing: self,
codingPath: codingPath,
wrapping: storage.pushKeyedContainer(keyed)
)
return KeyedEncodingContainer(container)
default:
preconditionFailure(
"""
No existing keyed encoding container to merge with.
"""
)
}
}
}

extension XMLEncoderImplementation {
Expand Down
4 changes: 2 additions & 2 deletions Sources/XMLCoder/Encoder/XMLEncodingStorage.swift
Expand Up @@ -31,8 +31,8 @@ struct XMLEncodingStorage {
return containers.last
}

mutating func pushKeyedContainer() -> SharedBox<KeyedBox> {
let container = SharedBox(KeyedBox())
mutating func pushKeyedContainer(_ keyedBox: KeyedBox = KeyedBox()) -> SharedBox<KeyedBox> {
let container = SharedBox(keyedBox)
containers.append(container)
return container
}
Expand Down
41 changes: 41 additions & 0 deletions Tests/XMLCoderTests/IntOrString.swift
@@ -0,0 +1,41 @@
//
// IntOrString.swift
// XMLCoderTests
//
// Created by Benjamin Wetherfield on 11/24/19.
//

import XMLCoder

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

extension IntOrString: Codable {
enum CodingKeys: String, CodingKey {
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))
}
}
}

extension IntOrString.CodingKeys: XMLChoiceCodingKey {}
107 changes: 107 additions & 0 deletions Tests/XMLCoderTests/MixedChoiceAndNonChoiceTests.swift
@@ -0,0 +1,107 @@
//
// MixedChoiceAndNonChoiceTests.swift
// XMLCoderTests
//
// Created by Benjamin Wetherfield on 11/24/19.
//

import XCTest
import XMLCoder

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

extension MixedIntOrStringFirst: Encodable {
enum CodingKeys: String, CodingKey {
case otherValue = "other-value"
}

func encode(to encoder: Encoder) throws {
try intOrString.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(otherValue, forKey: .otherValue)
}
}

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

extension MixedOtherFirst: Encodable {
enum CodingKeys: String, CodingKey {
case otherValue = "other-value"
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(otherValue, forKey: .otherValue)
try intOrString.encode(to: encoder)
}
}

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

extension MixedEitherSide: Encodable {
enum CodingKeys: String, CodingKey {
case leading
case trailing
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(leading, forKey: .leading)
try intOrString.encode(to: encoder)
try container.encode(trailing, forKey: .trailing)
}
}

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

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

class MixedChoiceAndNonChoiceTests: XCTestCase {
func testMixedChoiceFirstEncode() throws {
let first = MixedIntOrStringFirst(intOrString: .int(4), otherValue: "other")
let firstEncoded = try XMLEncoder().encode(first, withRootKey: "container")
let firstExpectedXML = "<container><int>4</int><other-value>other</other-value></container>"
XCTAssertEqual(String(data: firstEncoded, encoding: .utf8), firstExpectedXML)
}

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 testMixedChoiceFlankedEncode() throws {
let flanked = MixedEitherSide(leading: "first", intOrString: .string("then"), trailing: "second")
let flankedEncoded = try XMLEncoder().encode(flanked, withRootKey: "container")
let flankedExpectedXML = """
<container><leading>first</leading><string>then</string><trailing>second</trailing></container>
"""
XCTAssertEqual(String(data: flankedEncoded, encoding: .utf8), flankedExpectedXML)
}

func testTwoChoiceElementsEncode() throws {
let twoChoiceElements = TwoChoiceElements(first: .int(1), second: .string("one"))
let encoded = try XMLEncoder().encode(twoChoiceElements, withRootKey: "container")
let expectedXML = "<container><int>1</int><string>one</string></container>"
XCTAssertEqual(String(data: encoded, encoding: .utf8), expectedXML)
}
}
31 changes: 0 additions & 31 deletions Tests/XMLCoderTests/SimpleChoiceTests.swift
Expand Up @@ -8,37 +8,6 @@
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 = """
Expand Down
13 changes: 13 additions & 0 deletions Tests/XMLCoderTests/XCTestManifests.swift
Expand Up @@ -414,6 +414,18 @@ extension KeyedTests {
]
}

extension MixedChoiceAndNonChoiceTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__MixedChoiceAndNonChoiceTests = [
("testMixedChoiceFirstEncode", testMixedChoiceFirstEncode),
("testMixedChoiceFlankedEncode", testMixedChoiceFlankedEncode),
("testMixedChoiceSecondEncode", testMixedChoiceSecondEncode),
("testTwoChoiceElementsEncode", testTwoChoiceElementsEncode),
]
}

extension MixedContainerTest {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
Expand Down Expand Up @@ -840,6 +852,7 @@ public func __allTests() -> [XCTestCaseEntry] {
testCase(KeyedBoxTests.__allTests__KeyedBoxTests),
testCase(KeyedIntTests.__allTests__KeyedIntTests),
testCase(KeyedTests.__allTests__KeyedTests),
testCase(MixedChoiceAndNonChoiceTests.__allTests__MixedChoiceAndNonChoiceTests),
testCase(MixedContainerTest.__allTests__MixedContainerTest),
testCase(NameSpaceTest.__allTests__NameSpaceTest),
testCase(NestedAttributeChoiceTests.__allTests__NestedAttributeChoiceTests),
Expand Down