Skip to content

Commit

Permalink
Add DynamicNodeDecoding protocol (#85)
Browse files Browse the repository at this point in the history
This adds new `DynamicNodeDecoding` protocol similar to `DynamicNodeEncoding` introduced in #70 

* Add DynamicNodeDecoding protocol
* Remove NodeDecoding from XMLEncoder
* Add NodeDecoding to XMLDecoder
* Fix class name in DynamicNodeDecoding.swift
* Implement DynamicNodeDecoding with tests
* Improve test coverage
* Add more example code to README
* Fix wording in README
* Fix typos, cleanup example code
* Cleanup example code in README
  • Loading branch information
MaxDesiatov authored Mar 25, 2019
1 parent b8deb55 commit 40222d8
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 16 deletions.
126 changes: 124 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 40222d8

Please sign in to comment.