diff --git a/README.md b/README.md index 75eeccdc..654855e6 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 + + 123 + Cat in the Hat + Kids + Wildlife + +``` + +### Value coding key intrinsic + +Suppose that you need to decode an XML that looks similar to this: + +```xml + +456 +``` + +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 diff --git a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift new file mode 100644 index 00000000..748cdfeb --- /dev/null +++ b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift @@ -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 +} diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index 762c42be..7da6f2ca 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -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 @@ -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) } diff --git a/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift b/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift index 8ba34b5c..a1219b18 100644 --- a/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift +++ b/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift @@ -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 } diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index 6f1e5e39..ff386289 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -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 @@ -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. @@ -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) } diff --git a/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift b/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift index a10a4fe2..c9deedfd 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift @@ -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 { diff --git a/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift b/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift new file mode 100644 index 00000000..41b3cf49 --- /dev/null +++ b/Tests/XMLCoderTests/DynamicNodeDecodingTest.swift @@ -0,0 +1,214 @@ +// +// DynamicNodeEncodingTest.swift +// XMLCoderTests +// +// Created by Joseph Mattiello on 1/23/19. +// + +import Foundation +import XCTest +@testable import XMLCoder + +private let overlappingKeys = """ + + + + StringValue + + +""".data(using: .utf8)! + +private let libraryXMLYN = """ + + + + 123 + Cat in the Hat + Kids + Wildlife + + + 456 + 1984 + Classics + News + + +""".data(using: .utf8)! + +private let libraryXMLYNStrategy = """ + + + 2 + + 123 + +
true
+ Kids +
+ +
false
+ Wildlife +
+
+ + 456 + +
true
+ Classics +
+ +
false
+ News +
+
+
+""".data(using: .utf8)! + +private struct TestStruct: Codable, Equatable, DynamicNodeDecoding { + let attribute: Int + let element: String + + private enum CodingKeys: CodingKey { + case attribute + case element + + public var stringValue: String { + return "key" + } + } + + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { + switch key { + case CodingKeys.attribute: + return .attribute + default: + return .element + } + } +} + +private struct Library: Codable, Equatable, DynamicNodeDecoding { + let count: Int + let books: [Book] + + enum CodingKeys: String, CodingKey { + case count + case books = "book" + } + + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { + switch key { + case CodingKeys.count: + return .attribute + default: + return .element + } + } +} + +private 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 + } + } +} + +private struct Category: Codable, Equatable, DynamicNodeEncoding { + let main: Bool + let value: String + + private enum CodingKeys: String, CodingKey { + case main + case value + } + + static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case Category.CodingKeys.main: + return .attribute + default: + return .element + } + } +} + +final class DynamicNodeDecodingTest: XCTestCase { + func testDecode() throws { + let decoder = XMLDecoder() + decoder.errorContextLength = 10 + + let library = try decoder.decode(Library.self, from: libraryXMLYN) + XCTAssertEqual(library.books.count, 2) + XCTAssertEqual(library.count, 2) + + let book1 = library.books[0] + XCTAssertEqual(book1.id, 123) + XCTAssertEqual(book1.title, "Cat in the Hat") + + let book1Categories = book1.categories + XCTAssertEqual(book1Categories.count, 2) + XCTAssertEqual(book1Categories[0].value, "Kids") + XCTAssertTrue(book1Categories[0].main) + XCTAssertEqual(book1Categories[1].value, "Wildlife") + XCTAssertFalse(book1Categories[1].main) + + let book2 = library.books[1] + XCTAssertEqual(book2.id, 456) + XCTAssertEqual(book2.title, "1984") + + let book2Categories = book2.categories + XCTAssertEqual(book2Categories.count, 2) + XCTAssertEqual(book2Categories[0].value, "Classics") + XCTAssertTrue(book2Categories[0].main) + XCTAssertEqual(book2Categories[1].value, "News") + XCTAssertFalse(book2Categories[1].main) + } + + func testStrategyPriority() throws { + let decoder = XMLDecoder() + decoder.errorContextLength = 10 + + decoder.nodeDecodingStrategy = .custom { type, _ in + { key in + guard + type == Book.self && + key.stringValue == Book.CodingKeys.title.stringValue + else { + return .element + } + + return .attribute + } + } + + let library = try decoder.decode(Library.self, from: libraryXMLYNStrategy) + XCTAssertEqual(library.count, 2) + } + + func testOverlappingKeys() throws { + let decoder = XMLDecoder() + decoder.errorContextLength = 10 + + let test = try decoder.decode(TestStruct.self, from: overlappingKeys) + XCTAssertEqual(test, TestStruct(attribute: 123, element: "StringValue")) + } + + static var allTests = [ + ("testDecode", testDecode), + ("testStrategyPriority", testStrategyPriority), + ("testOverlappingKeys", testOverlappingKeys), + ] +} diff --git a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift index d28d9a29..586e616a 100644 --- a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift +++ b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift @@ -19,7 +19,7 @@ private let libraryXMLYN = """ Wildlife - 789 + 456 1984 Classics News @@ -27,6 +27,35 @@ private let libraryXMLYN = """ """.data(using: .utf8)! +private let libraryXMLYNStrategy = """ + + + 2 + + 123 + +
true
+ Kids +
+ +
false
+ Wildlife +
+
+ + 456 + +
true
+ Classics +
+ +
false
+ News +
+
+
+""" + private let libraryXMLTrueFalse = """ @@ -69,7 +98,7 @@ private struct Book: Codable, Equatable, DynamicNodeEncoding { let title: String let categories: [Category] - private enum CodingKeys: String, CodingKey { + enum CodingKeys: String, CodingKey { case id case title case categories = "category" @@ -152,7 +181,7 @@ final class DynamicNodeEncodingTest: XCTestCase { XCTAssertFalse(book1Categories[1].main) let book2 = library.books[1] - // XCTAssertEqual(book2.id, 456) + XCTAssertEqual(book2.id, 456) XCTAssertEqual(book2.title, "1984") let book2Categories = book2.categories @@ -186,7 +215,7 @@ final class DynamicNodeEncodingTest: XCTestCase { XCTAssertFalse(book1Categories[1].main) let book2 = library.books[1] - // XCTAssertEqual(book2.id, 456) + XCTAssertEqual(book2.id, 456) XCTAssertEqual(book2.title, "1984") let book2Categories = book2.categories @@ -203,6 +232,32 @@ final class DynamicNodeEncodingTest: XCTestCase { XCTAssertEqual(library, library2) } + func testStrategyPriority() throws { + let decoder = XMLDecoder() + decoder.errorContextLength = 10 + + let encoder = XMLEncoder() + encoder.outputFormatting = [.prettyPrinted] + encoder.nodeEncodingStrategy = .custom { type, _ in + { key in + guard + type == [Book].self && + key.stringValue == Book.CodingKeys.title.stringValue + else { + return .element + } + + return .attribute + } + } + + let library = try decoder.decode(Library.self, from: libraryXMLYN) + let data = try encoder.encode(library, withRootKey: "library", + header: XMLHeader(version: 1.0, + encoding: "UTF-8")) + XCTAssertEqual(String(data: data, encoding: .utf8)!, libraryXMLYNStrategy) + } + static var allTests = [ ("testEncode", testEncode), ("testDecode", testDecode), diff --git a/Tests/XMLCoderTests/SingleChildTests.swift b/Tests/XMLCoderTests/SingleChildTests.swift index 2848b0dc..8a9dab59 100644 --- a/Tests/XMLCoderTests/SingleChildTests.swift +++ b/Tests/XMLCoderTests/SingleChildTests.swift @@ -12,7 +12,7 @@ struct ProudParent: Codable, Equatable { var myChildAge: [Int] } -final class Test: XCTestCase { +final class SingleChildTest: XCTestCase { func testEncoder() throws { let encoder = XMLEncoder() diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index df9822e2..ac061ed8 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -85,7 +85,9 @@ BF9457F621CBB6BC005ACFDE /* KeyedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457EB21CBB6BC005ACFDE /* KeyedTests.swift */; }; BF9457F721CBB6BC005ACFDE /* DataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457EC21CBB6BC005ACFDE /* DataTests.swift */; }; D14D8A8621F1D6B300B0D31A /* SingleChildTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */; }; + D158F12F2229892C0032B449 /* DynamicNodeDecoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = D158F12E2229892C0032B449 /* DynamicNodeDecoding.swift */; }; D162674321F9B2AF0056D1D8 /* OptionalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D162674121F9B2850056D1D8 /* OptionalTests.swift */; }; + D1761D1F2247F04500F53CEF /* DynamicNodeDecodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1761D1E2247F04500F53CEF /* DynamicNodeDecodingTest.swift */; }; D1CB1EF521EA9599009CAF02 /* RJITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_37 /* RJITest.swift */; }; D1CFC8242226B13F00B03222 /* NamespaceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CFC8222226AFB400B03222 /* NamespaceTest.swift */; }; D1E0C85321D8E65E0042A261 /* ErrorContextTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1E0C85121D8E6540042A261 /* ErrorContextTest.swift */; }; @@ -200,7 +202,9 @@ D11979B621F5CD2500A9C574 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; D11979B721F5EA5400A9C574 /* XMLCoder.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = XMLCoder.podspec; sourceTree = ""; }; D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleChildTests.swift; sourceTree = ""; }; + D158F12E2229892C0032B449 /* DynamicNodeDecoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeDecoding.swift; sourceTree = ""; }; D162674121F9B2850056D1D8 /* OptionalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalTests.swift; sourceTree = ""; }; + D1761D1E2247F04500F53CEF /* DynamicNodeDecodingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeDecodingTest.swift; sourceTree = ""; }; D1CFC8222226AFB400B03222 /* NamespaceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamespaceTest.swift; sourceTree = ""; }; D1E0C85121D8E6540042A261 /* ErrorContextTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorContextTest.swift; sourceTree = ""; }; D1E0C85421D91EBF0042A261 /* Metatypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metatypes.swift; sourceTree = ""; }; @@ -387,6 +391,7 @@ D1FC040421C7EF8200065B43 /* RJISample.swift */, B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */, B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */, + D1761D1E2247F04500F53CEF /* DynamicNodeDecodingTest.swift */, A61DCCD621DF8DB300C0A19D /* ClassTests.swift */, D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */, D1CFC8222226AFB400B03222 /* NamespaceTest.swift */, @@ -448,6 +453,7 @@ OBJ_12 /* XMLDecodingStorage.swift */, OBJ_13 /* XMLKeyedDecodingContainer.swift */, OBJ_14 /* XMLUnkeyedDecodingContainer.swift */, + D158F12E2229892C0032B449 /* DynamicNodeDecoding.swift */, ); path = Decoder; sourceTree = ""; @@ -594,6 +600,7 @@ BF9457DA21CBB5D2005ACFDE /* DataBox.swift in Sources */, BF9457AB21CBB498005ACFDE /* DecimalBox.swift in Sources */, OBJ_56 /* XMLKeyedEncodingContainer.swift in Sources */, + D158F12F2229892C0032B449 /* DynamicNodeDecoding.swift in Sources */, D1E0C85521D91EBF0042A261 /* Metatypes.swift in Sources */, OBJ_57 /* XMLReferencingEncoder.swift in Sources */, BF9457BC21CBB4DB005ACFDE /* XMLCoderElement.swift in Sources */, @@ -634,6 +641,7 @@ D1CFC8242226B13F00B03222 /* NamespaceTest.swift in Sources */, BF9457D021CBB516005ACFDE /* UIntBoxTests.swift in Sources */, OBJ_80 /* BooksTest.swift in Sources */, + D1761D1F2247F04500F53CEF /* DynamicNodeDecodingTest.swift in Sources */, OBJ_81 /* BreakfastTest.swift in Sources */, OBJ_82 /* CDCatalog.swift in Sources */, BF63EF0021CCDED2001D38C5 /* XMLStackParserTests.swift in Sources */,