Skip to content

Commit

Permalink
Empty codingkey encode decode output (#267)
Browse files Browse the repository at this point in the history
* Fix encoding output when CodingKey is empty

* Fix decoding when CodingKey is empty

* Simplified and improved Choice decoding
  • Loading branch information
Alkenso authored Feb 12, 2024
1 parent ced9c53 commit f2c0d9f
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 90 deletions.
81 changes: 58 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ set a property `removeWhitespaceElements` to `true` (the default value is `false

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

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

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

extension IntOrString: Codable {

enum CodingKeys: String, XMLChoiceCodingKey {
case int
case string
}

enum IntCodingKeys: String, CodingKey { case _0 = "" }
enum StringCodingKeys: String, CodingKey { case _0 = "" }
}
```

This is described in more details in PR [\#119](https://github.com/CoreOffice/XMLCoder/pull/119)
by [@jsbean](https://github.com/jsbean) and [@bwetherfield](https://github.com/bwetherfield).

#### Choice elements with (inlined) complex associated values

Lets extend previous example replacing simple types with complex
in assosiated values. This example would cover XML like:

```xml
<container>
<nested attr="n1_a1">
<val>n1_v1</val>
<labeled>
<val>n2_val</val>
</labeled>
</nested>
<simple attr="n1_a1">
<val>n1_v1</val>
</simple>
</container>
```

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)
```swift
enum InlineChoice: Equatable, Codable {
case simple(Nested1)
case nested(Nested1, labeled: Nested2)

enum CodingKeys: String, CodingKey, XMLChoiceCodingKey {
case simple, nested
}

enum SimpleCodingKeys: String, CodingKey { case _0 = "" }

enum NestedCodingKeys: String, CodingKey {
case _0 = ""
case labeled
}

struct Nested1: Equatable, Codable, DynamicNodeEncoding {
var attr = "n1_a1"
var val = "n1_v1"

public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.attr: return .attribute
default: return .element
}
}
}

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))
}
struct Nested2: Equatable, Codable {
var val = "n2_val"
}
}
```

This is described in more details in PR [\#119](https://github.com/CoreOffice/XMLCoder/pull/119)
by [@jsbean](https://github.com/jsbean) and [@bwetherfield](https://github.com/bwetherfield).

### Integrating with [Combine](https://developer.apple.com/documentation/combine)

Starting with XMLCoder [version 0.9](https://github.com/CoreOffice/XMLCoder/releases/tag/0.9.0),
Expand Down
13 changes: 13 additions & 0 deletions Sources/XMLCoder/Auxiliaries/Utils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2018-2023 XMLCoder contributors
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
//
// Created by Alkenso (Vladimir Vashurkin) on 08.06.2023.
//

import Foundation

extension CodingKey {
internal var isInlined: Bool { stringValue == "" }
}
13 changes: 9 additions & 4 deletions Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ struct XMLCoderElement: Equatable {
return isStringNode || isCDATANode
}

private var isInlined: Bool {
return key.isEmpty
}

init(
key: String,
elements: [XMLCoderElement] = [],
Expand Down Expand Up @@ -193,8 +197,9 @@ struct XMLCoderElement: Equatable {
}

var string = ""
string += element._toXMLString(indented: level + 1, escapedCharacters, formatting, indentation)
string += prettyPrinted ? "\n" : ""
let indentLevel = isInlined ? level : level + 1
string += element._toXMLString(indented: indentLevel, escapedCharacters, formatting, indentation)
string += prettyPrinted && !isInlined ? "\n" : ""
return string
}

Expand Down Expand Up @@ -294,9 +299,9 @@ struct XMLCoderElement: Equatable {
let prettyPrinted = formatting.contains(.prettyPrinted)
let prefix: String
switch indentation {
case let .spaces(count) where prettyPrinted:
case let .spaces(count) where prettyPrinted && !isInlined:
prefix = String(repeating: " ", count: level * count)
case let .tabs(count) where prettyPrinted:
case let .tabs(count) where prettyPrinted && !isInlined:
prefix = String(repeating: "\t", count: level * count)
default:
prefix = ""
Expand Down
23 changes: 18 additions & 5 deletions Sources/XMLCoder/Decoder/XMLChoiceDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,24 @@ struct XMLChoiceDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol
public func nestedContainer<NestedKey>(
keyedBy _: NestedKey.Type, forKey key: Key
) throws -> KeyedDecodingContainer<NestedKey> {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: NestedKey.self,
reality: container
)
guard container.unboxed.key == key.stringValue else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: NestedKey.self,
reality: container
)
}

let value = container.unboxed.element
guard let container = XMLKeyedDecodingContainer<NestedKey>(box: value, decoder: decoder) else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
reality: value
)
}

return KeyedDecodingContainer(container)
}

public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
Expand Down
80 changes: 46 additions & 34 deletions Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,25 +133,7 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
))
}

let container: XMLKeyedDecodingContainer<NestedKey>
if let keyedContainer = value as? KeyedContainer {
container = XMLKeyedDecodingContainer<NestedKey>(
referencing: decoder,
wrapping: keyedContainer
)
} else if let keyedContainer = value as? KeyedBox {
container = XMLKeyedDecodingContainer<NestedKey>(
referencing: decoder,
wrapping: SharedBox(keyedContainer)
)
} else if let singleBox = value as? SingleKeyedBox {
let element = (singleBox.key, singleBox.element)
let keyedContainer = KeyedBox(elements: [element], attributes: [])
container = XMLKeyedDecodingContainer<NestedKey>(
referencing: decoder,
wrapping: SharedBox(keyedContainer)
)
} else {
guard let container = XMLKeyedDecodingContainer<NestedKey>(box: value, decoder: decoder) else {
throw DecodingError.typeMismatch(
at: codingPath,
expectation: [String: Any].self,
Expand Down Expand Up @@ -192,6 +174,32 @@ struct XMLKeyedDecodingContainer<K: CodingKey>: KeyedDecodingContainerProtocol {
}
}

extension XMLKeyedDecodingContainer {
internal init?(box: Box, decoder: XMLDecoderImplementation) {
switch box {
case let keyedContainer as KeyedContainer:
self.init(
referencing: decoder,
wrapping: keyedContainer
)
case let keyedBox as KeyedBox:
self.init(
referencing: decoder,
wrapping: SharedBox(keyedBox)
)
case let singleBox as SingleKeyedBox:
let element = (singleBox.key, singleBox.element)
let keyedContainer = KeyedBox(elements: [element], attributes: [])
self.init(
referencing: decoder,
wrapping: SharedBox(keyedContainer)
)
default:
return nil
}
}
}

/// Private functions
extension XMLKeyedDecodingContainer {
private func _errorDescription(of key: CodingKey) -> String {
Expand Down Expand Up @@ -248,7 +256,7 @@ extension XMLKeyedDecodingContainer {

let elements = container
.withShared { keyedBox -> [KeyedBox.Element] in
keyedBox.elements[key.stringValue].map {
return (key.isInlined ? keyedBox.elements.values : keyedBox.elements[key.stringValue]).map {
if let singleKeyed = $0 as? SingleKeyedBox {
return singleKeyed.element.isNull ? singleKeyed : singleKeyed.element
} else {
Expand All @@ -258,7 +266,7 @@ extension XMLKeyedDecodingContainer {
}

let attributes = container.withShared { keyedBox in
keyedBox.attributes[key.stringValue]
key.isInlined ? keyedBox.attributes.values : keyedBox.attributes[key.stringValue]
}

decoder.codingPath.append(key)
Expand All @@ -271,7 +279,6 @@ extension XMLKeyedDecodingContainer {
_ = decoder.nodeDecodings.removeLast()
decoder.codingPath.removeLast()
}
let box: Box

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

switch strategy(key) {
case .attribute?:
box = try getAttributeBox(for: type, attributes, key)
case .element?:
box = try getElementBox(for: type, elements, key)
case .elementOrAttribute?:
box = try getAttributeOrElementBox(attributes, elements, key)
default:
switch type {
case is XMLAttributeProtocol.Type:
let box: Box
if key.isInlined {
box = container.typeErasedUnbox()
} else {
switch strategy(key) {
case .attribute?:
box = try getAttributeBox(for: type, attributes, key)
case is XMLElementProtocol.Type:
case .element?:
box = try getElementBox(for: type, elements, key)
default:
case .elementOrAttribute?:
box = try getAttributeOrElementBox(attributes, elements, key)
default:
switch type {
case is XMLAttributeProtocol.Type:
box = try getAttributeBox(for: type, attributes, key)
case is XMLElementProtocol.Type:
box = try getElementBox(for: type, elements, key)
default:
box = try getAttributeOrElementBox(attributes, elements, key)
}
}
}

Expand Down
Loading

0 comments on commit f2c0d9f

Please sign in to comment.