Skip to content

Commit cf3c1f0

Browse files
bwetherfieldArjun Gupta
authored andcommitted
Mixed choice/non-choice encoding (CoreOffice#154)
## Overview Fixes bug encountered when encoding structs that hold a mixture of choice-element and non-choice-element (or multiple choice-element) properties. ## Example Given a structure that stores both a choice and non-choice property, ```swift private struct IntOrStringAndDouble: Equatable { let intOrString: IntOrString let decimal: Double } ``` the natural encoding approach (now available) is ```swift extension IntOrStringAndDouble: Encodable { enum CodingKeys: String, CodingKey { case decimal } func encode(to encoder: Encoder) { try intOrString.encode(to: encoder) var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(decimal, forKey: .decimal) } } ``` The following `encode` implementation also works: ```swift extension IntOrStringAndDouble: Encodable { enum CodingKeys: String, CodingKey { case decimal } func encode(to encoder: Encoder) { var singleValueContainer = encoder.singleValueContainer() try singleValueContainer.encode(intOrString) var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(decimal, forKey: .decimal) } } ``` `IntOrString` as defined in CoreOffice#119: ```swift enum IntOrString: Equatable { case int(Int) case string(String) } extension IntOrString: Encodable { 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) } } } extension IntOrString.CodingKeys: XMLChoiceCodingKey {} // signifies that `IntOrString` is a choice element ``` ## Implementation Details In cases where choice and non-choice elements (or multiple choice elements) co-exist in a keyed container, we merge them into a single `XMLKeyedEncodingContainer` (wrapping a `SharedBox<KeyedBox>`). Arrays of choice elements (using `XMLUnkeyedEncodingContainer` under the hood) are encoded the same way as before, as we do not hit the merging cases. For the array case, we still need the `XMLChoiceEncodingContainer` structure. ## Source Compatibility This is an additive change. * Add breaking case * Add choice and keyed merging encode functionality * Refactor * Fix commented code * Fix misnamed file * Fix xcode project * Fix precondition catch * Use switch syntax * Add multiple choice element case * Add explicit types in KeyedBox initialization * Add explicitly empty parameter to KeyedBox initializer * Use more concise type inference * Unify switch syntax * Cut down code duplication * Fix formatting
1 parent 464885d commit cf3c1f0

File tree

7 files changed

+224
-83
lines changed

7 files changed

+224
-83
lines changed

Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -64,58 +64,16 @@ class XMLEncoderImplementation: Encoder {
6464
// MARK: - Encoder Methods
6565

6666
public func container<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
67+
guard canEncodeNewValue else {
68+
return mergeWithExistingKeyedContainer(keyedBy: Key.self)
69+
}
6770
if Key.self is XMLChoiceCodingKey.Type {
6871
return choiceContainer(keyedBy: Key.self)
6972
} else {
7073
return keyedContainer(keyedBy: Key.self)
7174
}
7275
}
7376

74-
public func keyedContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
75-
// If an existing keyed container was already requested, return that one.
76-
let topContainer: SharedBox<KeyedBox>
77-
if canEncodeNewValue {
78-
// We haven't yet pushed a container at this level; do so here.
79-
topContainer = storage.pushKeyedContainer()
80-
} else {
81-
guard let container = storage.lastContainer as? SharedBox<KeyedBox> else {
82-
preconditionFailure(
83-
"""
84-
Attempt to push new keyed encoding container when already previously encoded \
85-
at this path.
86-
"""
87-
)
88-
}
89-
90-
topContainer = container
91-
}
92-
93-
let container = XMLKeyedEncodingContainer<Key>(referencing: self, codingPath: codingPath, wrapping: topContainer)
94-
return KeyedEncodingContainer(container)
95-
}
96-
97-
public func choiceContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
98-
let topContainer: SharedBox<ChoiceBox>
99-
if canEncodeNewValue {
100-
// We haven't yet pushed a container at this level; do so here.
101-
topContainer = storage.pushChoiceContainer()
102-
} else {
103-
guard let container = storage.lastContainer as? SharedBox<ChoiceBox> else {
104-
preconditionFailure(
105-
"""
106-
Attempt to push new (single element) keyed encoding container when already \
107-
previously encoded at this path.
108-
"""
109-
)
110-
}
111-
112-
topContainer = container
113-
}
114-
115-
let container = XMLChoiceEncodingContainer<Key>(referencing: self, codingPath: codingPath, wrapping: topContainer)
116-
return KeyedEncodingContainer(container)
117-
}
118-
11977
public func unkeyedContainer() -> UnkeyedEncodingContainer {
12078
// If an existing unkeyed container was already requested, return that one.
12179
let topContainer: SharedBox<UnkeyedBox>
@@ -141,6 +99,54 @@ class XMLEncoderImplementation: Encoder {
14199
public func singleValueContainer() -> SingleValueEncodingContainer {
142100
return self
143101
}
102+
103+
private func keyedContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
104+
let container = XMLKeyedEncodingContainer<Key>(
105+
referencing: self,
106+
codingPath: codingPath,
107+
wrapping: storage.pushKeyedContainer()
108+
)
109+
return KeyedEncodingContainer(container)
110+
}
111+
112+
private func choiceContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
113+
let container = XMLChoiceEncodingContainer<Key>(
114+
referencing: self,
115+
codingPath: codingPath,
116+
wrapping: storage.pushChoiceContainer()
117+
)
118+
return KeyedEncodingContainer(container)
119+
}
120+
121+
private func mergeWithExistingKeyedContainer<Key>(keyedBy _: Key.Type) -> KeyedEncodingContainer<Key> {
122+
switch storage.lastContainer {
123+
case let keyed as SharedBox<KeyedBox>:
124+
let container = XMLKeyedEncodingContainer<Key>(
125+
referencing: self,
126+
codingPath: codingPath,
127+
wrapping: keyed
128+
)
129+
return KeyedEncodingContainer(container)
130+
case let choice as SharedBox<ChoiceBox>:
131+
_ = storage.popContainer()
132+
let keyed = KeyedBox(
133+
elements: KeyedBox.Elements([choice.withShared { ($0.key, $0.element) }]),
134+
attributes: []
135+
)
136+
let container = XMLKeyedEncodingContainer<Key>(
137+
referencing: self,
138+
codingPath: codingPath,
139+
wrapping: storage.pushKeyedContainer(keyed)
140+
)
141+
return KeyedEncodingContainer(container)
142+
default:
143+
preconditionFailure(
144+
"""
145+
No existing keyed encoding container to merge with.
146+
"""
147+
)
148+
}
149+
}
144150
}
145151

146152
extension XMLEncoderImplementation {

Sources/XMLCoder/Encoder/XMLEncodingStorage.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ struct XMLEncodingStorage {
3131
return containers.last
3232
}
3333

34-
mutating func pushKeyedContainer() -> SharedBox<KeyedBox> {
35-
let container = SharedBox(KeyedBox())
34+
mutating func pushKeyedContainer(_ keyedBox: KeyedBox = KeyedBox()) -> SharedBox<KeyedBox> {
35+
let container = SharedBox(keyedBox)
3636
containers.append(container)
3737
return container
3838
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// IntOrString.swift
3+
// XMLCoderTests
4+
//
5+
// Created by Benjamin Wetherfield on 11/24/19.
6+
//
7+
8+
import XMLCoder
9+
10+
internal enum IntOrString: Equatable {
11+
case int(Int)
12+
case string(String)
13+
}
14+
15+
extension IntOrString: Codable {
16+
enum CodingKeys: String, CodingKey {
17+
case int
18+
case string
19+
}
20+
21+
func encode(to encoder: Encoder) throws {
22+
var container = encoder.container(keyedBy: CodingKeys.self)
23+
switch self {
24+
case let .int(value):
25+
try container.encode(value, forKey: .int)
26+
case let .string(value):
27+
try container.encode(value, forKey: .string)
28+
}
29+
}
30+
31+
init(from decoder: Decoder) throws {
32+
let container = try decoder.container(keyedBy: CodingKeys.self)
33+
do {
34+
self = .int(try container.decode(Int.self, forKey: .int))
35+
} catch {
36+
self = .string(try container.decode(String.self, forKey: .string))
37+
}
38+
}
39+
}
40+
41+
extension IntOrString.CodingKeys: XMLChoiceCodingKey {}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//
2+
// MixedChoiceAndNonChoiceTests.swift
3+
// XMLCoderTests
4+
//
5+
// Created by Benjamin Wetherfield on 11/24/19.
6+
//
7+
8+
import XCTest
9+
import XMLCoder
10+
11+
private struct MixedIntOrStringFirst: Equatable {
12+
let intOrString: IntOrString
13+
let otherValue: String
14+
}
15+
16+
extension MixedIntOrStringFirst: Encodable {
17+
enum CodingKeys: String, CodingKey {
18+
case otherValue = "other-value"
19+
}
20+
21+
func encode(to encoder: Encoder) throws {
22+
try intOrString.encode(to: encoder)
23+
var container = encoder.container(keyedBy: CodingKeys.self)
24+
try container.encode(otherValue, forKey: .otherValue)
25+
}
26+
}
27+
28+
private struct MixedOtherFirst: Equatable {
29+
let intOrString: IntOrString
30+
let otherValue: String
31+
}
32+
33+
extension MixedOtherFirst: Encodable {
34+
enum CodingKeys: String, CodingKey {
35+
case otherValue = "other-value"
36+
}
37+
38+
func encode(to encoder: Encoder) throws {
39+
var container = encoder.container(keyedBy: CodingKeys.self)
40+
try container.encode(otherValue, forKey: .otherValue)
41+
try intOrString.encode(to: encoder)
42+
}
43+
}
44+
45+
private struct MixedEitherSide {
46+
let leading: String
47+
let intOrString: IntOrString
48+
let trailing: String
49+
}
50+
51+
extension MixedEitherSide: Encodable {
52+
enum CodingKeys: String, CodingKey {
53+
case leading
54+
case trailing
55+
}
56+
57+
func encode(to encoder: Encoder) throws {
58+
var container = encoder.container(keyedBy: CodingKeys.self)
59+
try container.encode(leading, forKey: .leading)
60+
try intOrString.encode(to: encoder)
61+
try container.encode(trailing, forKey: .trailing)
62+
}
63+
}
64+
65+
private struct TwoChoiceElements {
66+
let first: IntOrString
67+
let second: IntOrString
68+
}
69+
70+
extension TwoChoiceElements: Encodable {
71+
func encode(to encoder: Encoder) throws {
72+
try first.encode(to: encoder)
73+
try second.encode(to: encoder)
74+
}
75+
}
76+
77+
class MixedChoiceAndNonChoiceTests: XCTestCase {
78+
func testMixedChoiceFirstEncode() throws {
79+
let first = MixedIntOrStringFirst(intOrString: .int(4), otherValue: "other")
80+
let firstEncoded = try XMLEncoder().encode(first, withRootKey: "container")
81+
let firstExpectedXML = "<container><int>4</int><other-value>other</other-value></container>"
82+
XCTAssertEqual(String(data: firstEncoded, encoding: .utf8), firstExpectedXML)
83+
}
84+
85+
func testMixedChoiceSecondEncode() throws {
86+
let second = MixedOtherFirst(intOrString: .int(4), otherValue: "other")
87+
let secondEncoded = try XMLEncoder().encode(second, withRootKey: "container")
88+
let secondExpectedXML = "<container><other-value>other</other-value><int>4</int></container>"
89+
XCTAssertEqual(String(data: secondEncoded, encoding: .utf8), secondExpectedXML)
90+
}
91+
92+
func testMixedChoiceFlankedEncode() throws {
93+
let flanked = MixedEitherSide(leading: "first", intOrString: .string("then"), trailing: "second")
94+
let flankedEncoded = try XMLEncoder().encode(flanked, withRootKey: "container")
95+
let flankedExpectedXML = """
96+
<container><leading>first</leading><string>then</string><trailing>second</trailing></container>
97+
"""
98+
XCTAssertEqual(String(data: flankedEncoded, encoding: .utf8), flankedExpectedXML)
99+
}
100+
101+
func testTwoChoiceElementsEncode() throws {
102+
let twoChoiceElements = TwoChoiceElements(first: .int(1), second: .string("one"))
103+
let encoded = try XMLEncoder().encode(twoChoiceElements, withRootKey: "container")
104+
let expectedXML = "<container><int>1</int><string>one</string></container>"
105+
XCTAssertEqual(String(data: encoded, encoding: .utf8), expectedXML)
106+
}
107+
}

Tests/XMLCoderTests/SimpleChoiceTests.swift

Lines changed: 0 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,6 @@
88
import XCTest
99
import XMLCoder
1010

11-
private enum IntOrString: Equatable {
12-
case int(Int)
13-
case string(String)
14-
}
15-
16-
extension IntOrString: Codable {
17-
enum CodingKeys: String, XMLChoiceCodingKey {
18-
case int
19-
case string
20-
}
21-
22-
func encode(to encoder: Encoder) throws {
23-
var container = encoder.container(keyedBy: CodingKeys.self)
24-
switch self {
25-
case let .int(value):
26-
try container.encode(value, forKey: .int)
27-
case let .string(value):
28-
try container.encode(value, forKey: .string)
29-
}
30-
}
31-
32-
init(from decoder: Decoder) throws {
33-
let container = try decoder.container(keyedBy: CodingKeys.self)
34-
do {
35-
self = .int(try container.decode(Int.self, forKey: .int))
36-
} catch {
37-
self = .string(try container.decode(String.self, forKey: .string))
38-
}
39-
}
40-
}
41-
4211
class SimpleChoiceTests: XCTestCase {
4312
func testIntOrStringIntDecoding() throws {
4413
let xml = """

Tests/XMLCoderTests/XCTestManifests.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,18 @@ extension KeyedTests {
414414
]
415415
}
416416

417+
extension MixedChoiceAndNonChoiceTests {
418+
// DO NOT MODIFY: This is autogenerated, use:
419+
// `swift test --generate-linuxmain`
420+
// to regenerate.
421+
static let __allTests__MixedChoiceAndNonChoiceTests = [
422+
("testMixedChoiceFirstEncode", testMixedChoiceFirstEncode),
423+
("testMixedChoiceFlankedEncode", testMixedChoiceFlankedEncode),
424+
("testMixedChoiceSecondEncode", testMixedChoiceSecondEncode),
425+
("testTwoChoiceElementsEncode", testTwoChoiceElementsEncode),
426+
]
427+
}
428+
417429
extension MixedContainerTest {
418430
// DO NOT MODIFY: This is autogenerated, use:
419431
// `swift test --generate-linuxmain`
@@ -840,6 +852,7 @@ public func __allTests() -> [XCTestCaseEntry] {
840852
testCase(KeyedBoxTests.__allTests__KeyedBoxTests),
841853
testCase(KeyedIntTests.__allTests__KeyedIntTests),
842854
testCase(KeyedTests.__allTests__KeyedTests),
855+
testCase(MixedChoiceAndNonChoiceTests.__allTests__MixedChoiceAndNonChoiceTests),
843856
testCase(MixedContainerTest.__allTests__MixedContainerTest),
844857
testCase(NameSpaceTest.__allTests__NameSpaceTest),
845858
testCase(NestedAttributeChoiceTests.__allTests__NestedAttributeChoiceTests),

0 commit comments

Comments
 (0)