Skip to content

Commit

Permalink
Update doc comments, add PropertyWrappersTest (#246)
Browse files Browse the repository at this point in the history
When viewing documentation published at https://swiftpackageindex.com/CoreOffice/XMLCoder/main/documentation/xmlcoder, I noticed that some of the doc comments are missing, and we made some of the protocols `public` by mistake. I've added doc comments with example code and update those protocols accordingly.

Additionally, tests and a section in `README.md` for recently added property wrappers were missing, that's fixed as well.
  • Loading branch information
MaxDesiatov committed Aug 25, 2022
1 parent ffd257a commit 1b15e5a
Show file tree
Hide file tree
Showing 11 changed files with 240 additions and 12 deletions.
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))
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

0 comments on commit 1b15e5a

Please sign in to comment.