Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -176,8 +180,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 @@ -277,9 +282,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