From 6465f0cd6b66558bf2e9038d49772bac5f9c1e26 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 17 Aug 2022 16:56:22 +0100 Subject: [PATCH 1/4] Update doc comments, add `PropertyWrappersTest` --- Sources/XMLCoder/Auxiliaries/Attribute.swift | 19 ++++- Sources/XMLCoder/Auxiliaries/Element.swift | 15 +++- .../Auxiliaries/ElementAndAttribute.swift | 18 ++++- Sources/XMLCoder/Auxiliaries/XMLHeader.swift | 3 + .../Decoder/DynamicNodeDecoding.swift | 33 +++++++++ Sources/XMLCoder/Decoder/XMLDecoder.swift | 10 ++- .../Encoder/DynamicNodeEncoding.swift | 34 +++++++++ Sources/XMLCoder/Encoder/XMLEncoder.swift | 7 +- .../PropertyWrappersTest.swift | 69 +++++++++++++++++++ XMLCoder.xcodeproj/project.pbxproj | 4 ++ 10 files changed, 200 insertions(+), 12 deletions(-) create mode 100644 Tests/XMLCoderTests/AdvancedFeatures/PropertyWrappersTest.swift 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..037bee09 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"`. 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 */, From 5dce0b2e81a249ea092810fe01e88d151fde77bb Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 17 Aug 2022 17:06:20 +0100 Subject: [PATCH 2/4] Add "Property wrappers" section to `README.md` --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index dacac4a3..92f16f68 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 former into the latter. + +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 From 80cb14bc39a4f02ae6623bfc81b561fd903bf26a Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 17 Aug 2022 17:09:16 +0100 Subject: [PATCH 3/4] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 92f16f68..2879f4ad 100644 --- a/README.md +++ b/README.md @@ -366,7 +366,7 @@ struct Book: Codable { ``` will encode value `Book(id: 42)` as `42`. And vice versa, -it will decode the former into the latter. +it will decode the latter into the former. Similarly, From 09e532c0809427f8afcd172d3872e465b5e1bfb8 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Wed, 24 Aug 2022 17:46:53 +0100 Subject: [PATCH 4/4] Update comment in `XMLDecoder.swift` --- Sources/XMLCoder/Decoder/XMLDecoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index 037bee09..1f67b269 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -257,7 +257,7 @@ open class XMLDecoder { /// 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"`. + /// of form `nodeName="value"`, with elements taking priority. case elementOrAttribute }