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

Add DynamicNodeDecoding protocol #85

Merged
merged 11 commits into from Mar 25, 2019
126 changes: 124 additions & 2 deletions README.md
Expand Up @@ -9,7 +9,7 @@ Encoder & Decoder for XML using Swift's `Codable` protocols.

This package is a fork of the original
[ShawnMoore/XMLParsing](https://github.com/ShawnMoore/XMLParsing)
with more options and tests added.
with more features and improved test coverage.

## Example

Expand Down Expand Up @@ -39,9 +39,131 @@ let note = try? XMLDecoder().decode(Note.self, from: data)
let returnData = try? XMLEncoder().encode(note, withRootKey: "note")
```

## Advanced features

### Dynamic node coding

XMLCoder provides two helper protocols that allow you to customize whether
nodes are encoded as attributes or elements: `DynamicNodeEncoding` and
`DynamicNodeDecoding`.

The declarations of the protocols are very simple:

```swift
protocol DynamicNodeEncoding: Encodable {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}

protocol DynamicNodeDecoding: Decodable {
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
}
```

The values returned by corresponding `static` functions look like this:

```swift
public enum NodeDecoding {
// decodes a value from an attribute
case attribute

// decodes a value from an element
case element

// the default, attempts to decode as an element first,
// otherwise reads from an attribute
case elementOrAttribute
}

enum NodeEncoding {
// encodes a value in an attribute
case attribute

// the default, encodes a value in an element
case element

// encodes a value in both attribute and element
case both
}
```

Add conformance to an appropriate protocol for types you'd like to customize.
Accordingly, this example code:

```swift
private struct Book: Codable, Equatable, DynamicNodeEncoding {
let id: UInt
let title: String
let categories: [Category]

private enum CodingKeys: String, CodingKey {
case id
case title
case categories = "category"
}

static func nodeEncoding(forKey key: CodingKey)
-> XMLEncoder.NodeEncoding {
switch key {
case Book.CodingKeys.id: return .both
default: return .element
}
}
}
```

works for this XML:

```xml
<book id="123">
<id>123</id>
<title>Cat in the Hat</title>
<category>Kids</category>
<category>Wildlife</category>
</book>
```

### Value coding key intrinsic

Suppose that you need to decode an XML that looks similar to this:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<foo id="123">456</foo>
```

By default you'd be able to decode `foo` as an element, but then it's not
possible to decode the `id` attribute. `XMLCoder` handles certain `CodingKey`
values in a special way to allow proper coding for this XML. Just add a coding
key with `stringValue` that equals `"value"` or `""` (empty string). What
follows is an example type declaration that encodes the XML above, but special
handling of coding keys with those values works for both encoding and decoding.

```swift
struct Foo: Codable, DynamicNodeEncoding {
let id: String
let value: String

enum CodingKeys: String, CodingKey {
case id
case value
// case value = "" would also work
}

static func nodeEncoding(forKey key: CodingKey)
-> XMLEncoder.NodeEncoding {
switch key {
case CodingKeys.id:
return .attribute
default:
return .element
}
}
}
```

## Installation

## Requirements
### Requirements

- Xcode 10
- Swift 4.2
Expand Down
10 changes: 10 additions & 0 deletions Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift
@@ -0,0 +1,10 @@
//
// DynamicNodeDecoding.swift
// XMLCoder
//
// Created by Max Desiatov on 01/03/2019.
//

public protocol DynamicNodeDecoding: Decodable {
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
}
7 changes: 5 additions & 2 deletions Sources/XMLCoder/Decoder/XMLDecoder.swift
Expand Up @@ -232,7 +232,7 @@ open class XMLDecoder {
/// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`.
open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys

/// A node's decoding tyoe
/// A node's decoding type
public enum NodeDecoding {
case attribute
case element
Expand All @@ -256,7 +256,10 @@ open class XMLDecoder {
) -> ((CodingKey) -> NodeDecoding) {
switch self {
case .deferredToDecoder:
return { _ in .elementOrAttribute }
guard let dynamicType = codableType as? DynamicNodeDecoding.Type else {
return { _ in .elementOrAttribute }
}
return dynamicType.nodeDecoding(for:)
case let .custom(closure):
return closure(codableType, decoder)
}
Expand Down
2 changes: 0 additions & 2 deletions Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift
Expand Up @@ -5,8 +5,6 @@
// Created by Joseph Mattiello on 1/24/19.
//

import Foundation

public protocol DynamicNodeEncoding: Encodable {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}
Expand Down
10 changes: 6 additions & 4 deletions Sources/XMLCoder/Encoder/XMLEncoder.swift
Expand Up @@ -33,7 +33,7 @@ open class XMLEncoder {
public static let sortedKeys = OutputFormatting(rawValue: 1 << 1)
}

/// A node's encoding tyoe
/// A node's encoding type
public enum NodeEncoding {
case attribute
case element
Expand Down Expand Up @@ -216,7 +216,7 @@ open class XMLEncoder {
@available(*, deprecated, renamed: "NodeEncodingStrategy")
public typealias NodeEncodingStrategies = NodeEncodingStrategy

public typealias XMLNodeEncoderClosure = ((CodingKey) -> XMLEncoder.NodeEncoding)
public typealias XMLNodeEncoderClosure = ((CodingKey) -> NodeEncoding)
public typealias XMLEncodingClosure = (Encodable.Type, Encoder) -> XMLNodeEncoderClosure

/// Set of strategies to use for encoding of nodes.
Expand All @@ -227,8 +227,10 @@ open class XMLEncoder {
/// Return a closure computing the desired node encoding for the value by its coding key.
case custom(XMLEncodingClosure)

func nodeEncodings(forType codableType: Encodable.Type,
with encoder: Encoder) -> ((CodingKey) -> XMLEncoder.NodeEncoding) {
func nodeEncodings(
forType codableType: Encodable.Type,
with encoder: Encoder
) -> ((CodingKey) -> NodeEncoding) {
return encoderClosure(codableType, encoder)
}

Expand Down
Expand Up @@ -12,7 +12,13 @@ extension XMLEncoderImplementation: SingleValueEncodingContainer {
// MARK: - SingleValueEncodingContainer Methods

func assertCanEncodeNewValue() {
precondition(canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.")
precondition(
canEncodeNewValue,
"""
Attempt to encode value through single value container when \
previously value already encoded.
"""
)
}

public func encodeNil() throws {
Expand Down