diff --git a/README.md b/README.md
index dacac4a3..2879f4ad 100644
--- a/README.md
+++ b/README.md
@@ -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 `42`. 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 `` 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 `42`. It will decode both
+`42` and `` 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
diff --git a/Sources/XMLCoder/Auxiliaries/Attribute.swift b/Sources/XMLCoder/Auxiliaries/Attribute.swift
index 95dae939..6f6dcdc5 100644
--- a/Sources/XMLCoder/Auxiliaries/Attribute.swift
+++ b/Sources/XMLCoder/Auxiliaries/Attribute.swift
@@ -5,9 +5,22 @@
// Created by Benjamin Wetherfield on 6/3/20.
//
-public protocol XMLAttributeProtocol {}
-
-@propertyWrapper public struct Attribute: 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 ``. And vice versa,
+ it will decode the former into the latter.
+ */
+@propertyWrapper
+public struct Attribute: XMLAttributeProtocol {
public var wrappedValue: Value
public init(_ wrappedValue: Value) {
diff --git a/Sources/XMLCoder/Auxiliaries/Element.swift b/Sources/XMLCoder/Auxiliaries/Element.swift
index d1e2d938..57a2b7dd 100644
--- a/Sources/XMLCoder/Auxiliaries/Element.swift
+++ b/Sources/XMLCoder/Auxiliaries/Element.swift
@@ -7,7 +7,20 @@
protocol XMLElementProtocol {}
-@propertyWrapper public struct Element: 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 `42`. And vice versa,
+ it will decode the former into the latter.
+ */
+@propertyWrapper
+public struct Element: XMLElementProtocol {
public var wrappedValue: Value
public init(_ wrappedValue: Value) {
diff --git a/Sources/XMLCoder/Auxiliaries/ElementAndAttribute.swift b/Sources/XMLCoder/Auxiliaries/ElementAndAttribute.swift
index 14de106f..69d6c02b 100644
--- a/Sources/XMLCoder/Auxiliaries/ElementAndAttribute.swift
+++ b/Sources/XMLCoder/Auxiliaries/ElementAndAttribute.swift
@@ -5,9 +5,23 @@
// Created by Benjamin Wetherfield on 6/7/20.
//
-public protocol XMLElementAndAttributeProtocol {}
+protocol XMLElementAndAttributeProtocol {}
-@propertyWrapper public struct ElementAndAttribute: 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 `42`. It will decode both
+ `42` and `` as `Book(id: 42)`.
+ */
+@propertyWrapper
+public struct ElementAndAttribute: XMLElementAndAttributeProtocol {
public var wrappedValue: Value
public init(_ wrappedValue: Value) {
diff --git a/Sources/XMLCoder/Auxiliaries/XMLHeader.swift b/Sources/XMLCoder/Auxiliaries/XMLHeader.swift
index acb12164..dd0e5915 100644
--- a/Sources/XMLCoder/Auxiliaries/XMLHeader.swift
+++ b/Sources/XMLCoder/Auxiliaries/XMLHeader.swift
@@ -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?
diff --git a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift
index 6c6584c9..9c87ba43 100644
--- a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift
+++ b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift
@@ -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
+
+ Cat in the Hat
+ Kids
+ Wildlife
+
+ ```
+ */
public protocol DynamicNodeDecoding: Decodable {
static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding
}
diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift
index 0df4969d..1f67b269 100644
--- a/Sources/XMLCoder/Decoder/XMLDecoder.swift
+++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift
@@ -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,
@@ -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,
@@ -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 {
@@ -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 `value`.
case element
+ /// Decodes a node from either elements of form `value` or attributes
+ /// of form `nodeName="value"`, with elements taking priority.
case elementOrAttribute
}
diff --git a/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift b/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift
index 3b0a5afd..79cbec6f 100644
--- a/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift
+++ b/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift
@@ -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
+
+ 123
+ Cat in the Hat
+ Kids
+ Wildlife
+
+ ```
+ */
public protocol DynamicNodeEncoding: Encodable {
static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding
}
diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift
index c15081a4..ac85a46b 100644
--- a/Sources/XMLCoder/Encoder/XMLEncoder.swift
+++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift
@@ -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
@@ -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.
@@ -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
diff --git a/Tests/XMLCoderTests/AdvancedFeatures/PropertyWrappersTest.swift b/Tests/XMLCoderTests/AdvancedFeatures/PropertyWrappersTest.swift
new file mode 100644
index 00000000..ef3cb4a1
--- /dev/null
+++ b/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 =
+ """
+
+ The Book
+ 24
+
+ """
+
+private let bookAuthorAttributeXML =
+ """
+
+ The Book
+
+ """
+
+private let bookAuthorElementXML =
+ """
+
+ 24
+ The 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)
+ }
+}
diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj
index 79d85e05..9eacbc40 100644
--- a/XMLCoder.xcodeproj/project.pbxproj
+++ b/XMLCoder.xcodeproj/project.pbxproj
@@ -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 */; };
@@ -208,6 +209,7 @@
D1A1839C24842D710058E66D /* PlantCatalog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlantCatalog.swift; sourceTree = ""; };
D1A1839D24842D710058E66D /* RJITest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RJITest.swift; sourceTree = ""; };
D1A1839E24842D710058E66D /* BorderTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BorderTest.swift; sourceTree = ""; };
+ D1B52EA428AD1DCB00004C56 /* PropertyWrappersTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyWrappersTest.swift; sourceTree = ""; };
OBJ_100 /* DecimalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalTests.swift; sourceTree = ""; };
OBJ_101 /* EmptyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyTests.swift; sourceTree = ""; };
OBJ_102 /* FloatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatTests.swift; sourceTree = ""; };
@@ -348,6 +350,7 @@
D1A1839024842D710058E66D /* DynamicNodeEncodingTest.swift */,
D1A1839124842D710058E66D /* CompositeChoiceTests.swift */,
D1A1839224842D710058E66D /* NestedChoiceArrayTest.swift */,
+ D1B52EA428AD1DCB00004C56 /* PropertyWrappersTest.swift */,
);
path = AdvancedFeatures;
sourceTree = "";
@@ -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 */,