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

Update doc comments, add PropertyWrappersTest #246

Merged
merged 4 commits into from Aug 25, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Expand Up @@ -353,6 +353,46 @@ The resulting XML will look like this:
This was implemented in PR [\#160](https://github.com/MaxDesiatov/XMLCoder/pull/160)
by [@portellaa](https://github.com/portellaa).

### Property wrappers

If your version of Swift allows property wrappers to be used, you may prefer this API to the more verbose
[dynamic node coding](#dynamic-node-coding).

For example, this type
```swift
struct Book: Codable {
@Element var id: Int
}
```

will encode value `Book(id: 42)` as `<Book><id>42</id></Book>`. And vice versa,
it will decode the latter into the former.

Similarly,

```swift
struct Book: Codable {
@Attribute var id: Int
}
```

will encode value `Book(id: 42)` as `<Book id="42"></Book>` and vice versa for decoding.

If you don't know upfront if a property will be present as an element or an attribute during decoding,
use `@ElementAndAttribute`:

```swift
struct Book: Codable {
@ElementAndAttribute var id: Int
}
```

This will encode value `Book(id: 42)` as `<Book id="42"><id>42</id></Book>`. It will decode both
`<Book><id>42</id></Book>` and `<Book id="42"></Book>` as `Book(id: 42)`.

This feature is available starting with XMLCoder 0.13.0 and was implemented
by [@bwetherfield](https://github.com/bwetherfield).

## Installation

### Requirements
Expand Down
19 changes: 16 additions & 3 deletions Sources/XMLCoder/Auxiliaries/Attribute.swift
Expand Up @@ -5,9 +5,22 @@
// Created by Benjamin Wetherfield on 6/3/20.
//

public protocol XMLAttributeProtocol {}

@propertyWrapper public struct Attribute<Value>: XMLAttributeProtocol {
protocol XMLAttributeProtocol {}

/** Property wrapper specifying that a given property should be encoded and decoded as an XML attribute.

For example, this type
```swift
struct Book: Codable {
@Attribute var id: Int
}
```

will encode value `Book(id: 42)` as `<Book id="42"></Book>`. And vice versa,
it will decode the former into the latter.
*/
@propertyWrapper
public struct Attribute<Value>: XMLAttributeProtocol {
public var wrappedValue: Value

public init(_ wrappedValue: Value) {
Expand Down
15 changes: 14 additions & 1 deletion Sources/XMLCoder/Auxiliaries/Element.swift
Expand Up @@ -7,7 +7,20 @@

protocol XMLElementProtocol {}

@propertyWrapper public struct Element<Value>: XMLElementProtocol {
/** Property wrapper specifying that a given property should be encoded and decoded as an XML element.

For example, this type
```swift
struct Book: Codable {
@Element var id: Int
}
```

will encode value `Book(id: 42)` as `<Book><id>42</id></Book>`. And vice versa,
it will decode the former into the latter.
*/
@propertyWrapper
public struct Element<Value>: XMLElementProtocol {
public var wrappedValue: Value

public init(_ wrappedValue: Value) {
Expand Down
18 changes: 16 additions & 2 deletions Sources/XMLCoder/Auxiliaries/ElementAndAttribute.swift
Expand Up @@ -5,9 +5,23 @@
// Created by Benjamin Wetherfield on 6/7/20.
//

public protocol XMLElementAndAttributeProtocol {}
protocol XMLElementAndAttributeProtocol {}

@propertyWrapper public struct ElementAndAttribute<Value>: XMLElementAndAttributeProtocol {
/** Property wrapper specifying that a given property should be decoded from either an XML element
or an XML attribute. When encoding, the value will be present as both an attribute, and an element.

For example, this type
```swift
struct Book: Codable {
@ElementAndAttribute var id: Int
}
```

will encode value `Book(id: 42)` as `<Book id="42"><id>42</id></Book>`. It will decode both
`<Book><id>42</id></Book>` and `<Book id="42"></Book>` as `Book(id: 42)`.
*/
@propertyWrapper
public struct ElementAndAttribute<Value>: XMLElementAndAttributeProtocol {
public var wrappedValue: Value

public init(_ wrappedValue: Value) {
Expand Down
3 changes: 3 additions & 0 deletions Sources/XMLCoder/Auxiliaries/XMLHeader.swift
Expand Up @@ -8,6 +8,9 @@

import Foundation

/// Type that allows overriding XML header during encoding. Pass a value of this type to the `encode`
/// function of `XMLEncoder` to specify the exact value of the header you'd like to see in the encoded
/// data.
public struct XMLHeader {
/// The XML standard that the produced document conforms to.
public let version: Double?
Expand Down
33 changes: 33 additions & 0 deletions Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift
Expand Up @@ -6,6 +6,39 @@
// Created by Max Desiatov on 01/03/2019.
//

/** Allows conforming types to specify how its properties will be decoded.

For example:
```swift
struct Book: Codable, Equatable, DynamicNodeDecoding {
let id: UInt
let title: String
let categories: [Category]

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

static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding {
switch key {
case Book.CodingKeys.id: return .attribute
default: return .element
}
}
}
```
allows XML of this form to be decoded into values of type `Book`:

```xml
<book id="123">
<title>Cat in the Hat</title>
<category>Kids</category>
<category>Wildlife</category>
</book>
```
*/
public protocol DynamicNodeDecoding: Decodable {
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
}
10 changes: 7 additions & 3 deletions Sources/XMLCoder/Decoder/XMLDecoder.swift
Expand Up @@ -39,7 +39,7 @@ open class XMLDecoder {
static func keyFormatted(
_ formatterForKey: @escaping (CodingKey) throws -> DateFormatter?
) -> XMLDecoder.DateDecodingStrategy {
return .custom { (decoder) -> Date in
return .custom { decoder -> Date in
guard let codingKey = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
Expand Down Expand Up @@ -90,7 +90,7 @@ open class XMLDecoder {
static func keyFormatted(
_ formatterForKey: @escaping (CodingKey) throws -> Data?
) -> XMLDecoder.DataDecodingStrategy {
return .custom { (decoder) -> Data in
return .custom { decoder -> Data in
guard let codingKey = decoder.codingPath.last else {
throw DecodingError.dataCorrupted(DecodingError.Context(
codingPath: decoder.codingPath,
Expand Down Expand Up @@ -179,7 +179,7 @@ open class XMLDecoder {
}

static func _convertFromUppercase(_ stringKey: String) -> String {
_convert(stringKey.lowercased(), usingSeparator: "_")
_convert(stringKey.lowercased(), usingSeparator: "_")
}

static func _convertFromSnakeCase(_ stringKey: String) -> String {
Expand Down Expand Up @@ -252,8 +252,12 @@ open class XMLDecoder {

/// A node's decoding type
public enum NodeDecoding {
/// Decodes a node from attributes of form `nodeName="value"`.
case attribute
/// Decodes a node from elements of form `<nodeName>value</nodeName>`.
case element
/// Decodes a node from either elements of form `<nodeName>value</nodeName>` or attributes
/// of form `nodeName="value"`, with elements taking priority.
case elementOrAttribute
}

Expand Down
34 changes: 34 additions & 0 deletions Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift
Expand Up @@ -6,6 +6,40 @@
// Created by Joseph Mattiello on 1/24/19.
//

/** Allows conforming types to specify how its properties will be encoded.

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

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

static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding {
switch key {
case Book.CodingKeys.id: return .both
default: return .element
}
}
}
```
produces XML of this form for values of type `Book`:

```xml
<book id="123">
<id>123</id>
<title>Cat in the Hat</title>
<category>Kids</category>
<category>Wildlife</category>
</book>
```
*/
public protocol DynamicNodeEncoding: Encodable {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}
Expand Down
7 changes: 4 additions & 3 deletions Sources/XMLCoder/Encoder/XMLEncoder.swift
Expand Up @@ -29,13 +29,13 @@ open class XMLEncoder {
public static let sortedKeys = OutputFormatting(rawValue: 1 << 1)
}

/// The identation to use when XML is pretty-printed.
/// The indentation to use when XML is pretty-printed.
public enum PrettyPrintIndentation {
case spaces(Int)
case tabs(Int)
}

/// A node's encoding type
/// A node's encoding type. Specifies how a node will be encoded.
public enum NodeEncoding {
case attribute
case element
Expand Down Expand Up @@ -230,7 +230,7 @@ open class XMLEncoder {
@available(*, deprecated, renamed: "NodeEncodingStrategy")
public typealias NodeEncodingStrategies = NodeEncodingStrategy

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

/// Set of strategies to use for encoding of nodes.
Expand Down Expand Up @@ -343,6 +343,7 @@ open class XMLEncoder {
/// - parameter withRootKey: the key used to wrap the encoded values. The
/// default value is inferred from the name of the root type.
/// - parameter rootAttributes: the list of attributes to be added to the root node
/// - parameter header: the XML header to start the encoded data with.
/// - returns: A new `Data` value containing the encoded XML data.
/// - throws: `EncodingError.invalidValue` if a non-conforming
/// floating-point value is encountered during encoding, and the encoding
Expand Down
69 changes: 69 additions & 0 deletions Tests/XMLCoderTests/AdvancedFeatures/PropertyWrappersTest.swift
@@ -0,0 +1,69 @@
//
// PropertyWrappersTest.swift
// XMLCoderTests
//
// Created by Max Desiatov on 17/08/2022.
//

import Foundation
import XCTest
import XMLCoder

private struct Book: Codable, Equatable {
@Attribute var id: Int
@Element var name: String
@ElementAndAttribute var authorID: Int

init(id: Int, name: String, authorID: Int) {
_id = Attribute(id)
_name = Element(name)
_authorID = ElementAndAttribute(authorID)
}
}

private let bookAuthorElementAndAttributeXML =
"""
<Book id="42" authorID="24">
<name>The Book</name>
<authorID>24</authorID>
</Book>
"""

private let bookAuthorAttributeXML =
"""
<Book id="42" authorID="24">
<name>The Book</name>
</Book>
"""

private let bookAuthorElementXML =
"""
<Book id="42">
<authorID>24</authorID>
<name>The Book</name>
</Book>
"""

private let book = Book(id: 42, name: "The Book", authorID: 24)

final class PropertyWrappersTest: XCTestCase {
func testEncode() throws {
let encoder = XMLEncoder()
encoder.outputFormatting = .prettyPrinted

let xml = try String(data: encoder.encode(book), encoding: .utf8)

XCTAssertEqual(bookAuthorElementAndAttributeXML, xml)
}

func testDecode() throws {
let decoder = XMLDecoder()
let decodedBookBoth = try decoder.decode(Book.self, from: Data(bookAuthorElementAndAttributeXML.utf8))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn’t we also add a from string decoding API, if one is seemingly lacking. That is not a change request, but a question

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a sensible addition and raises a question why JSONDecoder haven't got anything like this by now? XMLCoder was modelled after JSONDecoder and JSONEncoder APIs.

let decodedBookElement = try decoder.decode(Book.self, from: Data(bookAuthorElementXML.utf8))
let decodedBookAttribute = try decoder.decode(Book.self, from: Data(bookAuthorAttributeXML.utf8))

XCTAssertEqual(book, decodedBookBoth)
XCTAssertEqual(book, decodedBookElement)
XCTAssertEqual(book, decodedBookAttribute)
}
}
4 changes: 4 additions & 0 deletions XMLCoder.xcodeproj/project.pbxproj
Expand Up @@ -59,6 +59,7 @@
D1A183C824842DE80058E66D /* DynamicNodeEncodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1839024842D710058E66D /* DynamicNodeEncodingTest.swift */; };
D1A183C924842DE80058E66D /* CompositeChoiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1839124842D710058E66D /* CompositeChoiceTests.swift */; };
D1A183CA24842DE80058E66D /* NestedChoiceArrayTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A1839224842D710058E66D /* NestedChoiceArrayTest.swift */; };
D1B52EA528AD1DCB00004C56 /* PropertyWrappersTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1B52EA428AD1DCB00004C56 /* PropertyWrappersTest.swift */; };
OBJ_148 /* BoolBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* BoolBox.swift */; };
OBJ_149 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* Box.swift */; };
OBJ_150 /* ChoiceBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_14 /* ChoiceBox.swift */; };
Expand Down Expand Up @@ -208,6 +209,7 @@
D1A1839C24842D710058E66D /* PlantCatalog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlantCatalog.swift; sourceTree = "<group>"; };
D1A1839D24842D710058E66D /* RJITest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RJITest.swift; sourceTree = "<group>"; };
D1A1839E24842D710058E66D /* BorderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BorderTest.swift; sourceTree = "<group>"; };
D1B52EA428AD1DCB00004C56 /* PropertyWrappersTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyWrappersTest.swift; sourceTree = "<group>"; };
OBJ_100 /* DecimalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalTests.swift; sourceTree = "<group>"; };
OBJ_101 /* EmptyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTests.swift; sourceTree = "<group>"; };
OBJ_102 /* FloatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -348,6 +350,7 @@
D1A1839024842D710058E66D /* DynamicNodeEncodingTest.swift */,
D1A1839124842D710058E66D /* CompositeChoiceTests.swift */,
D1A1839224842D710058E66D /* NestedChoiceArrayTest.swift */,
D1B52EA428AD1DCB00004C56 /* PropertyWrappersTest.swift */,
);
path = AdvancedFeatures;
sourceTree = "<group>";
Expand Down Expand Up @@ -806,6 +809,7 @@
B5F74472233F74E400BBDB15 /* RootLevelAttributeTest.swift in Sources */,
D1A183C124842DE80058E66D /* AttributedEnumIntrinsicTest.swift in Sources */,
OBJ_244 /* DataTests.swift in Sources */,
D1B52EA528AD1DCB00004C56 /* PropertyWrappersTest.swift in Sources */,
OBJ_245 /* DateTests.swift in Sources */,
OBJ_246 /* DecimalTests.swift in Sources */,
D1A183C224842DE80058E66D /* NestedChoiceTests.swift in Sources */,
Expand Down