From 46801b65b14994cca5c6ef191d6a626941231d4e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 17 Mar 2019 19:16:03 +0000 Subject: [PATCH 01/10] Add DynamicNodeDecoding protocol --- .swiftformat | 1 + .../Decoder/DynamicNodeDecoding.swift | 22 +++++ Sources/XMLCoder/Decoder/XMLDecoder.swift | 8 +- .../Encoder/DynamicNodeEncoding.swift | 2 - Sources/XMLCoder/Encoder/XMLEncoder.swift | 19 +++- ...ntation+SingleValueEncodingContainer.swift | 8 +- .../DynamicNodeEncodingTest.swift | 93 +++++++++++++++---- XMLCoder.xcodeproj/project.pbxproj | 4 + 8 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift diff --git a/.swiftformat b/.swiftformat index cd0a2b10..492008ee 100644 --- a/.swiftformat +++ b/.swiftformat @@ -6,3 +6,4 @@ --operatorfunc nospace --ifdef noindent --stripunusedargs closure-only +--disable andOperator diff --git a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift new file mode 100644 index 00000000..9f3a4115 --- /dev/null +++ b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift @@ -0,0 +1,22 @@ +// +// DynamicNodeDecoding.swift +// XMLCoder +// +// Created by Max Desiatov on 01/03/2019. +// + +public protocol DynamicNodeDecoding: Encodable { + static func nodeDecoding(for key: CodingKey) -> XMLEncoder.NodeDecoding +} + +extension Array: DynamicNodeDecoding where Element: DynamicNodeDecoding { + public static func nodeDecoding(for key: CodingKey) -> XMLEncoder.NodeDecoding { + return Element.nodeDecoding(for: key) + } +} + +extension DynamicNodeDecoding where Self: Collection, Self.Iterator.Element: DynamicNodeDecoding { + public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeDecoding { + return Element.nodeDecoding(for: key) + } +} diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index 653c6c54..d55e335f 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -41,7 +41,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, @@ -72,7 +72,7 @@ open class XMLDecoder { debugDescription: "Cannot decode date string \(text)" ) } - }) + } } } @@ -91,7 +91,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, @@ -115,7 +115,7 @@ open class XMLDecoder { } return data - }) + } } } 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 8b87f0c5..ed66e8c7 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -33,6 +33,13 @@ open class XMLEncoder { public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) } + /// A node's decoding tyoe + public enum NodeDecoding { + case attribute + case element + case both + } + /// A node's encoding tyoe public enum NodeEncoding { case attribute @@ -194,9 +201,9 @@ open class XMLEncoder { searchRange = lowerCaseRange.upperBound.. 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 +234,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/DynamicNodeEncodingTest.swift b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift index 86dd3c16..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" @@ -104,19 +133,23 @@ private struct Category: Codable, Equatable, DynamicNodeEncoding { final class DynamicNodeEncodingTest: XCTestCase { func testEncode() throws { - let book1 = Book(id: 123, - title: "Cat in the Hat", - categories: [ - Category(main: true, value: "Kids"), - Category(main: false, value: "Wildlife"), - ]) - - let book2 = Book(id: 456, - title: "1984", - categories: [ - Category(main: true, value: "Classics"), - Category(main: false, value: "News"), - ]) + let book1 = Book( + id: 123, + title: "Cat in the Hat", + categories: [ + Category(main: true, value: "Kids"), + Category(main: false, value: "Wildlife"), + ] + ) + + let book2 = Book( + id: 456, + title: "1984", + categories: [ + Category(main: true, value: "Classics"), + Category(main: false, value: "News"), + ] + ) let library = Library(count: 2, books: [book1, book2]) let encoder = XMLEncoder() @@ -148,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 @@ -182,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 @@ -199,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/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 4a8f642d..1825cd4c 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ 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 */; }; D1CB1EF521EA9599009CAF02 /* RJITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_37 /* RJITest.swift */; }; D1CFC8242226B13F00B03222 /* NamespaceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1CFC8222226AFB400B03222 /* NamespaceTest.swift */; }; @@ -198,6 +199,7 @@ 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 = ""; }; 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 = ""; }; @@ -445,6 +447,7 @@ OBJ_12 /* XMLDecodingStorage.swift */, OBJ_13 /* XMLKeyedDecodingContainer.swift */, OBJ_14 /* XMLUnkeyedDecodingContainer.swift */, + D158F12E2229892C0032B449 /* DynamicNodeDecoding.swift */, ); path = Decoder; sourceTree = ""; @@ -591,6 +594,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 */, From f42512accfe909039990ca0342ce2c5057a77200 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 17 Mar 2019 19:28:07 +0000 Subject: [PATCH 02/10] Remove NodeDecoding from XMLEncoder --- Sources/XMLCoder/Encoder/XMLEncoder.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index ed66e8c7..c991141f 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -33,13 +33,6 @@ open class XMLEncoder { public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) } - /// A node's decoding tyoe - public enum NodeDecoding { - case attribute - case element - case both - } - /// A node's encoding tyoe public enum NodeEncoding { case attribute From 0191d6c37b6dd0381e035d56a63dc4fabb782cb4 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 17 Mar 2019 19:29:43 +0000 Subject: [PATCH 03/10] Add NoodeDecoding to XMLDecoder --- Sources/XMLCoder/Decoder/XMLDecoder.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index d55e335f..5b0f429b 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -232,6 +232,13 @@ open class XMLDecoder { /// The strategy to use for decoding keys. Defaults to `.useDefaultKeys`. open var keyDecodingStrategy: KeyDecodingStrategy = .useDefaultKeys + /// A node's decoding tyoe + public enum NodeDecoding { + case attribute + case element + case both + } + /// Contextual user-provided information for use during decoding. open var userInfo: [CodingUserInfoKey: Any] = [:] From 92b25c9b4303312bdd3ebf12b904c0437c5f053f Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 17 Mar 2019 21:58:05 +0000 Subject: [PATCH 04/10] Fix class name in DynamicNodeDecoding.swift --- Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift index 9f3a4115..a75a3e6a 100644 --- a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift +++ b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift @@ -6,17 +6,17 @@ // public protocol DynamicNodeDecoding: Encodable { - static func nodeDecoding(for key: CodingKey) -> XMLEncoder.NodeDecoding + static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding } extension Array: DynamicNodeDecoding where Element: DynamicNodeDecoding { - public static func nodeDecoding(for key: CodingKey) -> XMLEncoder.NodeDecoding { + public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { return Element.nodeDecoding(for: key) } } extension DynamicNodeDecoding where Self: Collection, Self.Iterator.Element: DynamicNodeDecoding { - public static func nodeEncoding(for key: CodingKey) -> XMLEncoder.NodeDecoding { + public static func nodeEncoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { return Element.nodeDecoding(for: key) } } From de36e3ad84d4d2af0f10a206830f4f3eebfd278e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 24 Mar 2019 18:10:47 +0000 Subject: [PATCH 05/10] Implement DynamicNodeDecoding with tests --- Sources/XMLCoder/Decoder/XMLDecoder.swift | 5 +- .../DynamicNodeDecodingTest.swift | 214 ++++++++++++++++++ Tests/XMLCoderTests/SingleChildTests.swift | 2 +- XMLCoder.xcodeproj/project.pbxproj | 4 + 4 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 Tests/XMLCoderTests/DynamicNodeDecodingTest.swift diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index 762c42be..c93733e1 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -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/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/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 1a0b1bb4..ac061ed8 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -87,6 +87,7 @@ 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 */; }; @@ -203,6 +204,7 @@ 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 = ""; }; @@ -389,6 +391,7 @@ D1FC040421C7EF8200065B43 /* RJISample.swift */, B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */, B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */, + D1761D1E2247F04500F53CEF /* DynamicNodeDecodingTest.swift */, A61DCCD621DF8DB300C0A19D /* ClassTests.swift */, D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */, D1CFC8222226AFB400B03222 /* NamespaceTest.swift */, @@ -638,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 */, From 6972ad3880a62fa3cc55e58b6f8ec97e6c351404 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 24 Mar 2019 18:34:16 +0000 Subject: [PATCH 06/10] Improve test coverage --- Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift index a75a3e6a..0650799e 100644 --- a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift +++ b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift @@ -8,15 +8,3 @@ public protocol DynamicNodeDecoding: Encodable { static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding } - -extension Array: DynamicNodeDecoding where Element: DynamicNodeDecoding { - public static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { - return Element.nodeDecoding(for: key) - } -} - -extension DynamicNodeDecoding where Self: Collection, Self.Iterator.Element: DynamicNodeDecoding { - public static func nodeEncoding(for key: CodingKey) -> XMLDecoder.NodeDecoding { - return Element.nodeDecoding(for: key) - } -} From 08350f5d5e557c2ca782ad54024162a499b33985 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 24 Mar 2019 19:28:39 +0000 Subject: [PATCH 07/10] Add more example code to README --- README.md | 126 +++++++++++++++++- .../Decoder/DynamicNodeDecoding.swift | 2 +- 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 75eeccdc..560e9a90 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 +} + +/// A node's encoding tyoe +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, +e.g. 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 index 0650799e..748cdfeb 100644 --- a/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift +++ b/Sources/XMLCoder/Decoder/DynamicNodeDecoding.swift @@ -5,6 +5,6 @@ // Created by Max Desiatov on 01/03/2019. // -public protocol DynamicNodeDecoding: Encodable { +public protocol DynamicNodeDecoding: Decodable { static func nodeDecoding(for key: CodingKey) -> XMLDecoder.NodeDecoding } From 755763e5ab013d43077554fdc7fd0809d4bd1021 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 24 Mar 2019 19:32:16 +0000 Subject: [PATCH 08/10] Fix wording in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 560e9a90..2eca6d77 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,8 @@ enum NodeEncoding { } ``` -Add conformance to an appropriate protocol for types you'd like to customize, -e.g. this example code: +Add conformance to an appropriate protocol for types you'd like to customize. +Accordingly, this example code: ```swift private struct Book: Codable, Equatable, DynamicNodeEncoding { From 6788797928a9b8dbac6999f12065b3d13a1d7b9d Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 24 Mar 2019 19:34:26 +0000 Subject: [PATCH 09/10] Fix typos, cleanup example code --- README.md | 1 - Sources/XMLCoder/Decoder/XMLDecoder.swift | 2 +- Sources/XMLCoder/Encoder/XMLEncoder.swift | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2eca6d77..873078e2 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,6 @@ public enum NodeDecoding { case elementOrAttribute } -/// A node's encoding tyoe enum NodeEncoding { // encodes a value in an attribute case attribute diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index c93733e1..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 diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index c991141f..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 From 49aca7679f1df542ab5ffe4728ee1de8354570f7 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sun, 24 Mar 2019 19:36:18 +0000 Subject: [PATCH 10/10] Cleanup example code in README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 873078e2..654855e6 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,8 @@ private struct Book: Codable, Equatable, DynamicNodeEncoding { case categories = "category" } - static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + static func nodeEncoding(forKey key: CodingKey) + -> XMLEncoder.NodeEncoding { switch key { case Book.CodingKeys.id: return .both default: return .element @@ -116,8 +117,8 @@ works for this XML: 123 Cat in the Hat - Kids - Wildlife + Kids + Wildlife ```