Skip to content

Commit f2c0d9f

Browse files
authored
Empty codingkey encode decode output (#267)
* Fix encoding output when CodingKey is empty * Fix decoding when CodingKey is empty * Simplified and improved Choice decoding
1 parent ced9c53 commit f2c0d9f

File tree

7 files changed

+254
-90
lines changed

7 files changed

+254
-90
lines changed

README.md

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -226,8 +226,8 @@ set a property `removeWhitespaceElements` to `true` (the default value is `false
226226

227227
Starting with [version 0.8](https://github.com/CoreOffice/XMLCoder/releases/tag/0.8.0),
228228
you can encode and decode `enum`s with associated values by conforming your
229-
`CodingKey` type additionally to `XMLChoiceCodingKey`. This allows decoding
230-
XML elements similar in structure to this example:
229+
`CodingKey` type additionally to `XMLChoiceCodingKey`. This allows encoding
230+
and decoding XML elements similar in structure to this example:
231231

232232
```xml
233233
<container>
@@ -242,41 +242,76 @@ XML elements similar in structure to this example:
242242
To decode these elements you can use this type:
243243

244244
```swift
245-
enum IntOrString: Equatable {
245+
enum IntOrString: Codable {
246246
case int(Int)
247247
case string(String)
248-
}
249-
250-
extension IntOrString: Codable {
248+
251249
enum CodingKeys: String, XMLChoiceCodingKey {
252250
case int
253251
case string
254252
}
253+
254+
enum IntCodingKeys: String, CodingKey { case _0 = "" }
255+
enum StringCodingKeys: String, CodingKey { case _0 = "" }
256+
}
257+
```
258+
259+
This is described in more details in PR [\#119](https://github.com/CoreOffice/XMLCoder/pull/119)
260+
by [@jsbean](https://github.com/jsbean) and [@bwetherfield](https://github.com/bwetherfield).
261+
262+
#### Choice elements with (inlined) complex associated values
263+
264+
Lets extend previous example replacing simple types with complex
265+
in assosiated values. This example would cover XML like:
266+
267+
```xml
268+
<container>
269+
<nested attr="n1_a1">
270+
<val>n1_v1</val>
271+
<labeled>
272+
<val>n2_val</val>
273+
</labeled>
274+
</nested>
275+
<simple attr="n1_a1">
276+
<val>n1_v1</val>
277+
</simple>
278+
</container>
279+
```
255280

256-
func encode(to encoder: Encoder) throws {
257-
var container = encoder.container(keyedBy: CodingKeys.self)
258-
switch self {
259-
case let .int(value):
260-
try container.encode(value, forKey: .int)
261-
case let .string(value):
262-
try container.encode(value, forKey: .string)
281+
```swift
282+
enum InlineChoice: Equatable, Codable {
283+
case simple(Nested1)
284+
case nested(Nested1, labeled: Nested2)
285+
286+
enum CodingKeys: String, CodingKey, XMLChoiceCodingKey {
287+
case simple, nested
288+
}
289+
290+
enum SimpleCodingKeys: String, CodingKey { case _0 = "" }
291+
292+
enum NestedCodingKeys: String, CodingKey {
293+
case _0 = ""
294+
case labeled
295+
}
296+
297+
struct Nested1: Equatable, Codable, DynamicNodeEncoding {
298+
var attr = "n1_a1"
299+
var val = "n1_v1"
300+
301+
public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
302+
switch key {
303+
case CodingKeys.attr: return .attribute
304+
default: return .element
305+
}
263306
}
264307
}
265308

266-
init(from decoder: Decoder) throws {
267-
let container = try decoder.container(keyedBy: CodingKeys.self)
268-
do {
269-
self = .int(try container.decode(Int.self, forKey: .int))
270-
} catch {
271-
self = .string(try container.decode(String.self, forKey: .string))
272-
}
309+
struct Nested2: Equatable, Codable {
310+
var val = "n2_val"
273311
}
274312
}
275313
```
276314

277-
This is described in more details in PR [\#119](https://github.com/CoreOffice/XMLCoder/pull/119)
278-
by [@jsbean](https://github.com/jsbean) and [@bwetherfield](https://github.com/bwetherfield).
279-
280315
### Integrating with [Combine](https://developer.apple.com/documentation/combine)
281316

282317
Starting with XMLCoder [version 0.9](https://github.com/CoreOffice/XMLCoder/releases/tag/0.9.0),
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) 2018-2023 XMLCoder contributors
2+
//
3+
// This software is released under the MIT License.
4+
// https://opensource.org/licenses/MIT
5+
//
6+
// Created by Alkenso (Vladimir Vashurkin) on 08.06.2023.
7+
//
8+
9+
import Foundation
10+
11+
extension CodingKey {
12+
internal var isInlined: Bool { stringValue == "" }
13+
}

Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ struct XMLCoderElement: Equatable {
3232
return isStringNode || isCDATANode
3333
}
3434

35+
private var isInlined: Bool {
36+
return key.isEmpty
37+
}
38+
3539
init(
3640
key: String,
3741
elements: [XMLCoderElement] = [],
@@ -193,8 +197,9 @@ struct XMLCoderElement: Equatable {
193197
}
194198

195199
var string = ""
196-
string += element._toXMLString(indented: level + 1, escapedCharacters, formatting, indentation)
197-
string += prettyPrinted ? "\n" : ""
200+
let indentLevel = isInlined ? level : level + 1
201+
string += element._toXMLString(indented: indentLevel, escapedCharacters, formatting, indentation)
202+
string += prettyPrinted && !isInlined ? "\n" : ""
198203
return string
199204
}
200205

@@ -294,9 +299,9 @@ struct XMLCoderElement: Equatable {
294299
let prettyPrinted = formatting.contains(.prettyPrinted)
295300
let prefix: String
296301
switch indentation {
297-
case let .spaces(count) where prettyPrinted:
302+
case let .spaces(count) where prettyPrinted && !isInlined:
298303
prefix = String(repeating: " ", count: level * count)
299-
case let .tabs(count) where prettyPrinted:
304+
case let .tabs(count) where prettyPrinted && !isInlined:
300305
prefix = String(repeating: "\t", count: level * count)
301306
default:
302307
prefix = ""

Sources/XMLCoder/Decoder/XMLChoiceDecodingContainer.swift

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,24 @@ struct XMLChoiceDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol
5959
public func nestedContainer<NestedKey>(
6060
keyedBy _: NestedKey.Type, forKey key: Key
6161
) throws -> KeyedDecodingContainer<NestedKey> {
62-
throw DecodingError.typeMismatch(
63-
at: codingPath,
64-
expectation: NestedKey.self,
65-
reality: container
66-
)
62+
guard container.unboxed.key == key.stringValue else {
63+
throw DecodingError.typeMismatch(
64+
at: codingPath,
65+
expectation: NestedKey.self,
66+
reality: container
67+
)
68+
}
69+
70+
let value = container.unboxed.element
71+
guard let container = XMLKeyedDecodingContainer<NestedKey>(box: value, decoder: decoder) else {
72+
throw DecodingError.typeMismatch(
73+
at: codingPath,
74+
expectation: [String: Any].self,
75+
reality: value
76+
)
77+
}
78+
79+
return KeyedDecodingContainer(container)
6780
}
6881

6982
public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {

Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -133,25 +133,7 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
133133
))
134134
}
135135

136-
let container: XMLKeyedDecodingContainer<NestedKey>
137-
if let keyedContainer = value as? KeyedContainer {
138-
container = XMLKeyedDecodingContainer<NestedKey>(
139-
referencing: decoder,
140-
wrapping: keyedContainer
141-
)
142-
} else if let keyedContainer = value as? KeyedBox {
143-
container = XMLKeyedDecodingContainer<NestedKey>(
144-
referencing: decoder,
145-
wrapping: SharedBox(keyedContainer)
146-
)
147-
} else if let singleBox = value as? SingleKeyedBox {
148-
let element = (singleBox.key, singleBox.element)
149-
let keyedContainer = KeyedBox(elements: [element], attributes: [])
150-
container = XMLKeyedDecodingContainer<NestedKey>(
151-
referencing: decoder,
152-
wrapping: SharedBox(keyedContainer)
153-
)
154-
} else {
136+
guard let container = XMLKeyedDecodingContainer<NestedKey>(box: value, decoder: decoder) else {
155137
throw DecodingError.typeMismatch(
156138
at: codingPath,
157139
expectation: [String: Any].self,
@@ -192,6 +174,32 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
192174
}
193175
}
194176

177+
extension XMLKeyedDecodingContainer {
178+
internal init?(box: Box, decoder: XMLDecoderImplementation) {
179+
switch box {
180+
case let keyedContainer as KeyedContainer:
181+
self.init(
182+
referencing: decoder,
183+
wrapping: keyedContainer
184+
)
185+
case let keyedBox as KeyedBox:
186+
self.init(
187+
referencing: decoder,
188+
wrapping: SharedBox(keyedBox)
189+
)
190+
case let singleBox as SingleKeyedBox:
191+
let element = (singleBox.key, singleBox.element)
192+
let keyedContainer = KeyedBox(elements: [element], attributes: [])
193+
self.init(
194+
referencing: decoder,
195+
wrapping: SharedBox(keyedContainer)
196+
)
197+
default:
198+
return nil
199+
}
200+
}
201+
}
202+
195203
/// Private functions
196204
extension XMLKeyedDecodingContainer {
197205
private func _errorDescription(of key: CodingKey) -> String {
@@ -248,7 +256,7 @@ extension XMLKeyedDecodingContainer {
248256

249257
let elements = container
250258
.withShared { keyedBox -> [KeyedBox.Element] in
251-
keyedBox.elements[key.stringValue].map {
259+
return (key.isInlined ? keyedBox.elements.values : keyedBox.elements[key.stringValue]).map {
252260
if let singleKeyed = $0 as? SingleKeyedBox {
253261
return singleKeyed.element.isNull ? singleKeyed : singleKeyed.element
254262
} else {
@@ -258,7 +266,7 @@ extension XMLKeyedDecodingContainer {
258266
}
259267

260268
let attributes = container.withShared { keyedBox in
261-
keyedBox.attributes[key.stringValue]
269+
key.isInlined ? keyedBox.attributes.values : keyedBox.attributes[key.stringValue]
262270
}
263271

264272
decoder.codingPath.append(key)
@@ -271,7 +279,6 @@ extension XMLKeyedDecodingContainer {
271279
_ = decoder.nodeDecodings.removeLast()
272280
decoder.codingPath.removeLast()
273281
}
274-
let box: Box
275282

276283
// You can't decode sequences from attributes, but other strategies
277284
// need special handling for empty sequences.
@@ -292,21 +299,26 @@ extension XMLKeyedDecodingContainer {
292299
return ((cdata as? StringBox)?.unboxed as? T) ?? emptyString
293300
}
294301

295-
switch strategy(key) {
296-
case .attribute?:
297-
box = try getAttributeBox(for: type, attributes, key)
298-
case .element?:
299-
box = try getElementBox(for: type, elements, key)
300-
case .elementOrAttribute?:
301-
box = try getAttributeOrElementBox(attributes, elements, key)
302-
default:
303-
switch type {
304-
case is XMLAttributeProtocol.Type:
302+
let box: Box
303+
if key.isInlined {
304+
box = container.typeErasedUnbox()
305+
} else {
306+
switch strategy(key) {
307+
case .attribute?:
305308
box = try getAttributeBox(for: type, attributes, key)
306-
case is XMLElementProtocol.Type:
309+
case .element?:
307310
box = try getElementBox(for: type, elements, key)
308-
default:
311+
case .elementOrAttribute?:
309312
box = try getAttributeOrElementBox(attributes, elements, key)
313+
default:
314+
switch type {
315+
case is XMLAttributeProtocol.Type:
316+
box = try getAttributeBox(for: type, attributes, key)
317+
case is XMLElementProtocol.Type:
318+
box = try getElementBox(for: type, elements, key)
319+
default:
320+
box = try getAttributeOrElementBox(attributes, elements, key)
321+
}
310322
}
311323
}
312324

0 commit comments

Comments
 (0)