From 0a8a07ae8430ddc815c788277b0c780055c1ff3b Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:19:20 -0500 Subject: [PATCH 01/24] Bool: Support init from y,n,yes,no any case, false,true added uppercase support --- Sources/XMLCoder/Auxiliaries/Box/BoolBox.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/Box/BoolBox.swift b/Sources/XMLCoder/Auxiliaries/Box/BoolBox.swift index 6412bf0c..9ab936b5 100644 --- a/Sources/XMLCoder/Auxiliaries/Box/BoolBox.swift +++ b/Sources/XMLCoder/Auxiliaries/Box/BoolBox.swift @@ -17,9 +17,9 @@ struct BoolBox: Equatable { } init?(xmlString: String) { - switch xmlString { - case "false", "0": self.init(false) - case "true", "1": self.init(true) + switch xmlString.lowercased() { + case "false", "0", "n", "no": self.init(false) + case "true", "1", "y", "yes": self.init(true) case _: return nil } } From ae4d50ae3db57a92e71d13dc0627aef65feaf8e6 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:31:24 -0500 Subject: [PATCH 02/24] KeyedEncoding: Add new string formatters, capitalized, lowercased, uppercased Some helpers to deal with converting auto-generated codable String values for instance variables to match some common XML key coding standards to the commonly used Swift camel casing - capitalzied: convert first letter to uppercase - uppercased: All letters uppercased - lowercased: All letters lowercased Support all types that conform to StringProtocol rather than just String --- .../Auxiliaries/String+Extensions.swift | 29 +++++++++++++++++-- Sources/XMLCoder/Encoder/XMLEncoder.swift | 24 +++++++++++++++ .../Encoder/XMLKeyedEncodingContainer.swift | 12 ++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/String+Extensions.swift b/Sources/XMLCoder/Auxiliaries/String+Extensions.swift index 99fc8c48..867637ca 100644 --- a/Sources/XMLCoder/Auxiliaries/String+Extensions.swift +++ b/Sources/XMLCoder/Auxiliaries/String+Extensions.swift @@ -7,9 +7,9 @@ import Foundation -extension String { +extension StringProtocol where Self.Index == String.Index { func escape(_ characterSet: [(character: String, escapedCharacter: String)]) -> String { - var string = self + var string = String(self) for set in characterSet { string = string.replacingOccurrences(of: set.character, with: set.escapedCharacter, options: .literal) @@ -18,3 +18,28 @@ extension String { return string } } + +extension StringProtocol { + func capitalizingFirstLetter() -> Self { + guard count > 1 else { + return self + } + return Self(prefix(1).uppercased() + self.dropFirst())! + } + + mutating func capitalizeFirstLetter() { + self = self.capitalizingFirstLetter() + } + + func lowercasingFirstLetter() -> Self { + // avoid lowercasing single letters (I), or capitalized multiples (AThing ! to aThing, leave as AThing) + guard count > 1 && !(String(prefix(2)) == prefix(2).uppercased()) else { + return self + } + return Self(prefix(1).lowercased() + self.dropFirst())! + } + + mutating func lowercaseFirstLetter() { + self = self.capitalizingFirstLetter() + } +} diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index eeb12d0a..e2fe3017 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -122,6 +122,18 @@ open class XMLEncoder { /// - Note: Using a key encoding strategy has a nominal performance cost, as each string key has to be converted. case convertToSnakeCase + /// Capitalize the first letter only + /// `oneTwoThree` becomes `OneTwoThree` + case capitalized + + /// Uppercase ize all letters + /// `oneTwoThree` becomes `ONETWOTHREE` + case uppercased + + /// Lowercase all letters + /// `oneTwoThree` becomes `onetwothree` + case lowercased + /// Provide a custom conversion to the key in the encoded XML from the /// keys specified by the encoded types. /// The full path to the current encoding position is provided for @@ -186,6 +198,18 @@ open class XMLEncoder { }).joined(separator: "_") return result } + + static func _convertToCapitalized(_ stringKey: String) -> String { + return stringKey.capitalizingFirstLetter() + } + + static func _convertToLowercased(_ stringKey: String) -> String { + return stringKey.lowercased() + } + + static func _convertToUppercased(_ stringKey: String) -> String { + return stringKey.uppercased() + } } @available(*, deprecated, renamed: "NodeEncodingStrategy") diff --git a/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift b/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift index 7279a6b7..fc03f5c6 100644 --- a/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift +++ b/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift @@ -46,6 +46,18 @@ struct XMLKeyedEncodingContainer: KeyedEncodingContainerProtocol { return XMLKey(stringValue: newKeyString, intValue: key.intValue) case let .custom(converter): return converter(codingPath + [key]) + case .capitalized: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToCapitalized(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case .uppercased: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToUppercased(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) + case .lowercased: + let newKeyString = XMLEncoder.KeyEncodingStrategy + ._convertToLowercased(key.stringValue) + return XMLKey(stringValue: newKeyString, intValue: key.intValue) } } From e65d30b2e8146f660a77983778033e293671a423 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:38:09 -0500 Subject: [PATCH 03/24] SharedBoxProtocol: Generalize for any Box inheritance Use a type erased protocl inheritance strategy commonly used to provide default implimentation to avaoid issues with as? checks in generic protocols, while retaining reuse benefits of generic protocols --- Sources/XMLCoder/Auxiliaries/Box/Box.swift | 15 +++++++++++++-- Sources/XMLCoder/Auxiliaries/Box/SharedBox.swift | 2 +- Sources/XMLCoder/Encoder/XMLEncoder.swift | 4 ++-- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/Box/Box.swift b/Sources/XMLCoder/Auxiliaries/Box/Box.swift index 8dfdb829..7f3e78ab 100644 --- a/Sources/XMLCoder/Auxiliaries/Box/Box.swift +++ b/Sources/XMLCoder/Auxiliaries/Box/Box.swift @@ -17,6 +17,17 @@ protocol SimpleBox: Box { // A simple tagging protocol, for now. } -protocol SharedBoxProtocol { - func unbox() -> Box +protocol TypeErasedSharedBoxProtocol { + func typeErasedUnbox() -> Box +} + +protocol SharedBoxProtocol: TypeErasedSharedBoxProtocol { + associatedtype B: Box + func unbox() -> B +} + +extension SharedBoxProtocol { + func typeErasedUnbox() -> Box { + return unbox() + } } diff --git a/Sources/XMLCoder/Auxiliaries/Box/SharedBox.swift b/Sources/XMLCoder/Auxiliaries/Box/SharedBox.swift index 8106a15c..ea215705 100644 --- a/Sources/XMLCoder/Auxiliaries/Box/SharedBox.swift +++ b/Sources/XMLCoder/Auxiliaries/Box/SharedBox.swift @@ -30,7 +30,7 @@ extension SharedBox: Box { } extension SharedBox: SharedBoxProtocol { - func unbox() -> Box { + func unbox() -> Unboxed { return unboxed } } diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index e2fe3017..06882042 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -632,10 +632,10 @@ extension XMLEncoderImplementation { let lastContainer = storage.popContainer() - guard let sharedBox = lastContainer as? SharedBoxProtocol else { + guard let sharedBox = lastContainer as? TypeErasedSharedBoxProtocol else { return lastContainer } - return sharedBox.unbox() + return sharedBox.typeErasedUnbox() } } From 99fde81b5fdb6b5b73e18cf0a25080c74cbb9e7a Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:40:09 -0500 Subject: [PATCH 04/24] Remove junk string in BreakfastTest xml --- Tests/XMLCoderTests/BreakfastTest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/XMLCoderTests/BreakfastTest.swift b/Tests/XMLCoderTests/BreakfastTest.swift index 17ebea24..dba56019 100644 --- a/Tests/XMLCoderTests/BreakfastTest.swift +++ b/Tests/XMLCoderTests/BreakfastTest.swift @@ -17,7 +17,7 @@ private let xml = """ $5.95 Two of our famous Belgian Waffles with plenty of real maple syrup - /Users/vincent/Projects/Swift/XMLCoder/Tests/XMLCoderTests/NotesTest.swift + Strawberry Belgian Waffles $7.95 From 38b79a284662ef0311b91fad67bebf44f1b03327 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:50:46 -0500 Subject: [PATCH 05/24] Element coding, remove empty brackets if element string value is empty string In the case where a codable provides an empty string for the codable string value for an instance variable an empty bracket was inserted which is invalid XML. ``` let attr = "bar" let value = "FOO" enum CodingKeys : String, CodingKey { case attr case value = "" } ``` Will be useful for unkeyed objects that contain only attributes eg; ```xml <>FOO FOO ``` --- .../XMLCoder/Auxiliaries/XMLCoderElement.swift | 17 +++++++++++++---- Sources/XMLCoder/Decoder/XMLDecoder.swift | 4 ++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 7301e3b9..4afdeb32 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -314,25 +314,34 @@ struct XMLCoderElement: Equatable { repeating: " ", count: (prettyPrinted ? level : 0) * 4 ) var string = indentation - string += "<\(key)" + if !key.isEmpty { + string += "<\(key)" + } formatXMLAttributes(formatting, &string) if let value = value { - string += ">" + if !key.isEmpty { + string += ">" + } if !ignoreEscaping { string += (cdata == true ? "" : "\(value.escape(XMLCoderElement.escapedCharacterSet))") } else { string += "\(value)" } - string += "" + + if !key.isEmpty { + string += "" + } } else if !elements.isEmpty { string += prettyPrinted ? ">\n" : ">" formatXMLElements(formatting, &string, level, cdata, prettyPrinted) string += indentation - string += "" + if !key.isEmpty { + string += "" + } } else { string += " />" } diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index 525e04ef..2435e1b6 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -655,6 +655,10 @@ extension XMLDecoderImplementation { } else if type == Decimal.self || type == NSDecimalNumber.self { let decimal: Decimal = try unbox(box) decoded = decimal as? T + } else if + type == String.self || type == NSString.self, + let str: String = try? unbox(box), let value = str as? T { + decoded = value } else { storage.push(container: box) decoded = try type.init(from: self) From 0300dd1d627547b5a617427a8f574cbacde79a4e Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:57:53 -0500 Subject: [PATCH 06/24] Add DynamicNodeEncoding protocol DynamicNodeEncoding allows easily adding the ability to choose if iVars should be attribute or element encoded by inheriting DynamicNodeEncoding and implimenting a single static function in any Codable class or struct. This is simpler than the current method that requires a global dynamic encoding closure for every XMLEncoder instance. This allows changing the encoding where the data models live, rather than the creator of the XMLEncoder instance needing to have knowledge of all the possible structs and classes that the encoder might encounter at init time. --- .../Encoder/DynamicNodeEncoding.swift | 30 ++++ .../DynamicNodeEncodingTest.swift | 160 ++++++++++++++++++ XMLCoder.xcodeproj/project.pbxproj | 8 + 3 files changed, 198 insertions(+) create mode 100644 Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift create mode 100644 Tests/XMLCoderTests/DynamicNodeEncodingTest.swift diff --git a/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift b/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift new file mode 100644 index 00000000..2293d658 --- /dev/null +++ b/Sources/XMLCoder/Encoder/DynamicNodeEncoding.swift @@ -0,0 +1,30 @@ +// +// DynamicNodeEncoding.swift +// XMLCoder +// +// Created by Joseph Mattiello on 1/24/19. +// + +import Foundation + +public protocol DynamicNodeEncoding: Encodable { + static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding +} + +public extension DynamicNodeEncoding { + static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + return XMLEncoder.NodeEncoding.default + } +} + +extension Array: DynamicNodeEncoding where Element: DynamicNodeEncoding { + public static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + return Element.nodeEncoding(forKey: key) + } +} + +extension DynamicNodeEncoding where Self: Collection, Self.Iterator.Element: DynamicNodeEncoding { + public static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + return Element.nodeEncoding(forKey: key) + } +} diff --git a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift new file mode 100644 index 00000000..0e74471a --- /dev/null +++ b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift @@ -0,0 +1,160 @@ +// +// SOAPSample.swift +// XMLCoderTests +// +// Created by Joseph Mattiello on 1/23/19. +// + +import Foundation +import XCTest +@testable import XMLCoder + +let libraryXML = """ + + + + 123 + Cat in the Hat + Kids + Wildlife + + + 789 + 1984 + Classics + News + + +""".data(using: .utf8)! + +private struct Library: Codable, Equatable { + let count: Int + let books: [Book] + + private enum CodingKeys: String, CodingKey { + case count + case books = "book" + } +} + +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 + } + } +} + +extension Book { + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(UInt.self, forKey: .id) + title = try container.decode(String.self, forKey: .title) + + var nested = try container.nestedUnkeyedContainer(forKey: .categories) + + var decoded = [Category]() + var finished = false + + while !finished { + do { + let another = try nested.decode(Category.self) + decoded.append(another) + } catch DecodingError.valueNotFound { + finished = true + } catch { + throw error + } + } + + categories = decoded + } +} + +private struct Category: Codable, Equatable, DynamicNodeEncoding { + let main: Bool + let value: String + + private enum CodingKeys: String, CodingKey { + case main + case value = "" + } + + static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case Category.CodingKeys.main: + return .attribute + default: + return .element + } + } +} + +private func decodeArray(_ decoder: Decoder, decode: (inout UnkeyedDecodingContainer) throws -> T) throws -> [T] { + let keyedContainer = try decoder.container(keyedBy: CodingKeys.self) + var container = try keyedContainer.nestedUnkeyedContainer(forKey: .value) + + var decoded = [T]() + var finished = false + + while !finished { + do { + decoded.append(try decode(&container)) + } catch DecodingError.valueNotFound { + finished = true + } catch { + throw error + } + } + + return decoded +} + +final class IntrinsicTest: XCTestCase { + + func testEncode() { + 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() + encoder.outputFormatting = [.prettyPrinted] + + let header = XMLHeader(version: 1.0, encoding: "UTF-8") + do { + let encoded = try encoder.encode(library, withRootKey: "library", header: header) + let xmlString = String(data: encoded, encoding: .utf8) + XCTAssertNotNil(xmlString) + print(xmlString!) + } catch { + print("Test threw error: " + error.localizedDescription) + XCTFail(error.localizedDescription) + } + } + + static var allTests = [ + ("testEncode", testEncode), + ] +} diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 2b03c9e3..1bd98f4f 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -24,6 +24,8 @@ A61DCCD821DF9CA200C0A19D /* ClassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61DCCD621DF8DB300C0A19D /* ClassTests.swift */; }; A61FE03921E4D60B0015D993 /* UnkeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */; }; A61FE03C21E4EAB10015D993 /* KeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */; }; + B35157CE21F986DD009CA0CC /* DynamicNodeEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */; }; + B3BE1D612202C1F600259831 /* DynamicNodeEncodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */; }; BF63EF0021CCDED2001D38C5 /* XMLStackParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63EEFF21CCDED2001D38C5 /* XMLStackParserTests.swift */; }; BF63EF0621CD7A74001D38C5 /* URLBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63EF0521CD7A74001D38C5 /* URLBox.swift */; }; BF63EF0821CD7AF8001D38C5 /* URLBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63EF0721CD7AF8001D38C5 /* URLBoxTests.swift */; }; @@ -126,6 +128,8 @@ A61DCCD621DF8DB300C0A19D /* ClassTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassTests.swift; sourceTree = ""; }; A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnkeyedIntTests.swift; sourceTree = ""; }; A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = ""; }; + B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncoding.swift; sourceTree = ""; }; + B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncodingTest.swift; sourceTree = ""; }; BF63EEFF21CCDED2001D38C5 /* XMLStackParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLStackParserTests.swift; sourceTree = ""; }; BF63EF0521CD7A74001D38C5 /* URLBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBox.swift; sourceTree = ""; }; BF63EF0721CD7AF8001D38C5 /* URLBoxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBoxTests.swift; sourceTree = ""; }; @@ -329,6 +333,7 @@ OBJ_19 /* XMLKeyedEncodingContainer.swift */, OBJ_20 /* XMLReferencingEncoder.swift */, OBJ_21 /* XMLUnkeyedEncodingContainer.swift */, + B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */, ); path = Encoder; sourceTree = ""; @@ -360,6 +365,7 @@ OBJ_38 /* RelationshipsTest.swift */, BF63EF1D21CEC99A001D38C5 /* BenchmarkTests.swift */, D1FC040421C7EF8200065B43 /* RJISample.swift */, + B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */, A61DCCD621DF8DB300C0A19D /* ClassTests.swift */, D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */, ); @@ -549,6 +555,7 @@ OBJ_53 /* EncodingErrorExtension.swift in Sources */, BF9457B921CBB4DB005ACFDE /* XMLStackParser.swift in Sources */, OBJ_54 /* XMLEncoder.swift in Sources */, + B35157CE21F986DD009CA0CC /* DynamicNodeEncoding.swift in Sources */, BF9457BA21CBB4DB005ACFDE /* ISO8601DateFormatter.swift in Sources */, OBJ_55 /* XMLEncodingStorage.swift in Sources */, BF9457A921CBB498005ACFDE /* KeyedBox.swift in Sources */, @@ -611,6 +618,7 @@ OBJ_85 /* NodeEncodingStrategyTests.swift in Sources */, OBJ_86 /* NoteTest.swift in Sources */, BF63EF0C21CD7F28001D38C5 /* EmptyTests.swift in Sources */, + B3BE1D612202C1F600259831 /* DynamicNodeEncodingTest.swift in Sources */, BF9457F721CBB6BC005ACFDE /* DataTests.swift in Sources */, BF9457EE21CBB6BC005ACFDE /* IntTests.swift in Sources */, BF8171F221D3D03E00901EB0 /* SharedBoxTests.swift in Sources */, From 14549a65314c39d8c186c395c57601cf7b5d3620 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:28:22 -0500 Subject: [PATCH 07/24] XMLEncoder: Add both option to value encoding, refactor encoder - refactor element and attribute encoders to closures for easier code reuse - Added type alias for encoding closures for clarity --- Sources/XMLCoder/Encoder/XMLEncoder.swift | 29 +++++++++++------ .../Encoder/XMLKeyedEncodingContainer.swift | 31 ++++++++++++++----- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index 06882042..820b1d8d 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -37,6 +37,7 @@ open class XMLEncoder { public enum NodeEncoding { case attribute case element + case both public static let `default`: NodeEncoding = .element } @@ -215,24 +216,34 @@ open class XMLEncoder { @available(*, deprecated, renamed: "NodeEncodingStrategy") public typealias NodeEncodingStrategies = NodeEncodingStrategy + public typealias XMLNodeEncoderClosure = ((CodingKey) -> XMLEncoder.NodeEncoding) + public typealias XMLEncodingClosure = (Encodable.Type, Encoder) -> XMLNodeEncoderClosure + /// Set of strategies to use for encoding of nodes. public enum NodeEncodingStrategy { /// Defer to `Encoder` for choosing an encoding. This is the default strategy. case deferredToEncoder /// Return a closure computing the desired node encoding for the value by its coding key. - case custom((Encodable.Type, Encoder) -> ((CodingKey) -> XMLEncoder.NodeEncoding)) + case custom(XMLEncodingClosure) + + func nodeEncodings(forType codableType: Encodable.Type, + with encoder: Encoder) -> ((CodingKey) -> XMLEncoder.NodeEncoding) { + return self.encoderClosure(codableType, encoder) + } - func nodeEncodings( - forType codableType: Encodable.Type, - with encoder: Encoder - ) -> ((CodingKey) -> XMLEncoder.NodeEncoding) { + var encoderClosure: XMLEncodingClosure { switch self { - case .deferredToEncoder: - return { _ in .default } - case let .custom(closure): - return closure(codableType, encoder) + case .deferredToEncoder: return NodeEncodingStrategy.defaultEncoder + case let .custom(closure): return closure + } + } + + static let defaultEncoder: XMLEncodingClosure = { codableType, encoder in + guard let dynamicType = codableType as? DynamicNodeEncoding.Type else { + return { _ in return .default } } + return dynamicType.nodeEncoding(forKey:) } } diff --git a/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift b/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift index fc03f5c6..a5229898 100644 --- a/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift +++ b/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift @@ -99,22 +99,39 @@ struct XMLKeyedEncodingContainer: KeyedEncodingContainerProtocol { ) encoder.nodeEncodings.append(nodeEncodings) let box = try encode(encoder, value) - switch strategy(key) { - case .attribute: + + let mySelf = self + let attributeEncoder: (T, Key, Box) throws -> () = { value, key, box in guard let attribute = box as? SimpleBox else { throw EncodingError.invalidValue(value, EncodingError.Context( codingPath: [], debugDescription: "Complex values cannot be encoded as attributes." )) } - container.withShared { container in - container.attributes[_converted(key).stringValue] = attribute + mySelf.container.withShared { container in + container.attributes[mySelf._converted(key).stringValue] = attribute } - case .element: - container.withShared { container in - container.elements[_converted(key).stringValue] = box + } + + let elementEncoder: (T, Key, Box) throws -> () = { value, key, box in + mySelf.container.withShared { container in + container.elements[mySelf._converted(key).stringValue] = box } } + + defer { + self = mySelf + } + + switch strategy(key) { + case .attribute: + try attributeEncoder(value, key, box) + case .element: + try elementEncoder(value, key, box) + case .both: + try attributeEncoder(value, key, box) + try elementEncoder(value, key, box) + } } public mutating func nestedContainer( From f8c594a7df1750cdab28b608ddf3f5d3f31d0cb9 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:21:19 -0500 Subject: [PATCH 08/24] XMLDecoder.XMLDecodingStorage refactor initial value to inline var --- Sources/XMLCoder/Decoder/XMLDecoder.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index 2435e1b6..f99665d7 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -303,7 +303,7 @@ class XMLDecoderImplementation: Decoder { // MARK: Properties /// The decoder's storage. - var storage: XMLDecodingStorage + var storage: XMLDecodingStorage = XMLDecodingStorage() /// Options set on the top-level decoder. let options: XMLDecoder.Options @@ -323,7 +323,6 @@ class XMLDecoderImplementation: Decoder { /// Initializes `self` with the given top-level container and options. init(referencing container: Box, at codingPath: [CodingKey] = [], options: XMLDecoder.Options) { - storage = XMLDecodingStorage() storage.push(container: container) self.codingPath = codingPath self.options = options From 2ee4b03cf73a79fe9cb73d45aefc38855964263d Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 01:45:14 -0500 Subject: [PATCH 09/24] Clear up most swiftlint warnings --- .../XMLCoder/Auxiliaries/Box/KeyedBox.swift | 27 +- .../Auxiliaries/XMLCoderElement.swift | 187 +++++---- Sources/XMLCoder/Decoder/XMLDecoder.swift | 375 ------------------ ...ntation+SingleValueDecodingContainer.swift | 53 +++ .../Decoder/XMLDecoderImplementation.swift | 340 ++++++++++++++++ Sources/XMLCoder/Encoder/XMLEncoder.swift | 308 -------------- ...ntation+SingleValueEncodingContainer.swift | 97 +++++ .../Encoder/XMLEncoderImplementation.swift | 229 +++++++++++ XMLCoder.xcodeproj/project.pbxproj | 16 + 9 files changed, 840 insertions(+), 792 deletions(-) create mode 100644 Sources/XMLCoder/Decoder/XMLDecoderImplementation+SingleValueDecodingContainer.swift create mode 100644 Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift create mode 100644 Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift create mode 100644 Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift diff --git a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift index 3a94f6dd..a779e0f8 100644 --- a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift +++ b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift @@ -70,30 +70,27 @@ struct KeyedBox { typealias Attributes = KeyedStorage typealias Elements = KeyedStorage - var attributes: Attributes = [:] var elements: Elements = [:] + var attributes: Attributes = [:] - init() { - attributes = [:] - elements = [:] + func unbox() -> (elements: Elements, attributes: Attributes) { + return ( + elements: elements, + attributes: attributes + ) } +} +extension KeyedBox { init(elements: E, attributes: A) where E: Sequence, E.Element == (Key, Element), A: Sequence, A.Element == (Key, Attribute) { - self.elements = Elements(Dictionary(uniqueKeysWithValues: elements)) - self.attributes = Attributes(Dictionary(uniqueKeysWithValues: attributes)) + let elements = Elements(Dictionary(uniqueKeysWithValues: elements)) + let attributes = Attributes(Dictionary(uniqueKeysWithValues: attributes)) + self.init(elements: elements, attributes: attributes) } init(elements: [Key: Element], attributes: [Key: Attribute]) { - self.elements = Elements(elements) - self.attributes = Attributes(attributes) - } - - func unbox() -> (elements: Elements, attributes: Attributes) { - return ( - elements: elements, - attributes: attributes - ) + self.init(elements: Elements(elements), attributes: Attributes(attributes)) } } diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 4afdeb32..0a685acd 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -27,90 +27,13 @@ struct XMLCoderElement: Equatable { value: String? = nil, elements: [XMLCoderElement] = [], attributes: [String: String] = [:] - ) { + ) { self.key = key self.value = value self.elements = elements self.attributes = attributes } - init(key: String, box: UnkeyedBox) { - let elements = box.map { box in - XMLCoderElement(key: key, box: box) - } - - self.init(key: key, elements: elements) - } - - init(key: String, box: KeyedBox) { - var elements: [XMLCoderElement] = [] - - for (key, box) in box.elements { - let fail = { - preconditionFailure("Unclassified box: \(type(of: box))") - } - - switch box { - case let sharedUnkeyedBox as SharedBox: - guard let box = sharedUnkeyedBox.unbox() as? UnkeyedBox else { - fail() - } - elements.append(contentsOf: box.map { - XMLCoderElement(key: key, box: $0) - }) - case let unkeyedBox as UnkeyedBox: - // This basically injects the unkeyed children directly into self: - elements.append(contentsOf: unkeyedBox.map { - XMLCoderElement(key: key, box: $0) - }) - case let sharedKeyedBox as SharedBox: - guard let box = sharedKeyedBox.unbox() as? KeyedBox else { - fail() - } - elements.append(XMLCoderElement(key: key, box: box)) - case let keyedBox as KeyedBox: - elements.append(XMLCoderElement(key: key, box: keyedBox)) - case let simpleBox as SimpleBox: - elements.append(XMLCoderElement(key: key, box: simpleBox)) - default: - fail() - } - } - - let attributes: [String: String] = Dictionary( - uniqueKeysWithValues: box.attributes.compactMap { key, box in - guard let value = box.xmlString() else { - return nil - } - return (key, value) - } - ) - - self.init(key: key, elements: elements, attributes: attributes) - } - - init(key: String, box: SimpleBox) { - self.init(key: key) - value = box.xmlString() - } - - init(key: String, box: Box) { - switch box { - case let sharedUnkeyedBox as SharedBox: - self.init(key: key, box: sharedUnkeyedBox.unbox()) - case let sharedKeyedBox as SharedBox: - self.init(key: key, box: sharedKeyedBox.unbox()) - case let unkeyedBox as UnkeyedBox: - self.init(key: key, box: unkeyedBox) - case let keyedBox as KeyedBox: - self.init(key: key, box: keyedBox) - case let simpleBox as SimpleBox: - self.init(key: key, box: simpleBox) - case let box: - preconditionFailure("Unclassified box: \(type(of: box))") - } - } - mutating func append(value string: String) { var value = self.value ?? "" value += string.trimmingCharacters(in: .whitespacesAndNewlines) @@ -124,9 +47,8 @@ struct XMLCoderElement: Equatable { func flatten() -> KeyedBox { let attributes = self.attributes.mapValues { StringBox($0) } - var elements: [String: Box] = [:] - - for element in self.elements { + let keyedElements: [String: Box] = self.elements.reduce([String: Box]()) { (result, element) -> [String: Box] in + var result = result let key = element.key let hasValue = element.value != nil @@ -135,42 +57,43 @@ struct XMLCoderElement: Equatable { if hasValue || hasElements || hasAttributes { if let content = element.value { - switch elements[key] { + switch result[key] { case var unkeyedBox as UnkeyedBox: unkeyedBox.append(StringBox(content)) - elements[key] = unkeyedBox + result[key] = unkeyedBox case let stringBox as StringBox: - elements[key] = UnkeyedBox([stringBox, StringBox(content)]) + result[key] = UnkeyedBox([stringBox, StringBox(content)]) default: - elements[key] = StringBox(content) + result[key] = StringBox(content) } } if hasElements || hasAttributes { let content = element.flatten() - switch elements[key] { + switch result[key] { case var unkeyedBox as UnkeyedBox: unkeyedBox.append(content) - elements[key] = unkeyedBox + result[key] = unkeyedBox case let box?: - elements[key] = UnkeyedBox([box, content]) + result[key] = UnkeyedBox([box, content]) default: - elements[key] = content + result[key] = content } } } else { - switch elements[key] { + switch result[key] { case var unkeyedBox as UnkeyedBox: unkeyedBox.append(NullBox()) - elements[key] = unkeyedBox + result[key] = unkeyedBox case let box?: - elements[key] = UnkeyedBox([box, NullBox()]) + result[key] = UnkeyedBox([box, NullBox()]) default: - elements[key] = NullBox() + result[key] = NullBox() } } + return result } - let keyedBox = KeyedBox(elements: elements, attributes: attributes) + let keyedBox = KeyedBox(elements: keyedElements, attributes: attributes) return keyedBox } @@ -349,3 +272,79 @@ struct XMLCoderElement: Equatable { return string } } + +// MARK: - Convenience Initializers +extension XMLCoderElement { + init(key: String, box: UnkeyedBox) { + let elements = box.map { box in + XMLCoderElement(key: key, box: box) + } + + self.init(key: key, elements: elements) + } + + init(key: String, box: KeyedBox) { + var elements: [XMLCoderElement] = [] + + for (key, box) in box.elements { + let fail = { + preconditionFailure("Unclassified box: \(type(of: box))") + } + + switch box { + case let sharedUnkeyedBox as SharedBox: + let box = sharedUnkeyedBox.unbox() + elements.append(contentsOf: box.map { + XMLCoderElement(key: key, box: $0) + }) + case let unkeyedBox as UnkeyedBox: + // This basically injects the unkeyed children directly into self: + elements.append(contentsOf: unkeyedBox.map { + XMLCoderElement(key: key, box: $0) + }) + case let sharedKeyedBox as SharedBox: + let box = sharedKeyedBox.unbox() + elements.append(XMLCoderElement(key: key, box: box)) + case let keyedBox as KeyedBox: + elements.append(XMLCoderElement(key: key, box: keyedBox)) + case let simpleBox as SimpleBox: + elements.append(XMLCoderElement(key: key, box: simpleBox)) + default: + fail() + } + } + + let attributes: [String: String] = Dictionary( + uniqueKeysWithValues: box.attributes.compactMap { key, box in + guard let value = box.xmlString() else { + return nil + } + return (key, value) + } + ) + + self.init(key: key, elements: elements, attributes: attributes) + } + + init(key: String, box: SimpleBox) { + self.init(key: key) + value = box.xmlString() + } + + init(key: String, box: Box) { + switch box { + case let sharedUnkeyedBox as SharedBox: + self.init(key: key, box: sharedUnkeyedBox.unbox()) + case let sharedKeyedBox as SharedBox: + self.init(key: key, box: sharedKeyedBox.unbox()) + case let unkeyedBox as UnkeyedBox: + self.init(key: key, box: unkeyedBox) + case let keyedBox as KeyedBox: + self.init(key: key, box: keyedBox) + case let simpleBox as SimpleBox: + self.init(key: key, box: simpleBox) + case let box: + preconditionFailure("Unclassified box: \(type(of: box))") + } + } +} diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index f99665d7..5f13f006 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -298,378 +298,3 @@ open class XMLDecoder { return box } } - -class XMLDecoderImplementation: Decoder { - // MARK: Properties - - /// The decoder's storage. - var storage: XMLDecodingStorage = XMLDecodingStorage() - - /// Options set on the top-level decoder. - let options: XMLDecoder.Options - - /// The path to the current point in encoding. - public internal(set) var codingPath: [CodingKey] - - /// Contextual user-provided information for use during encoding. - public var userInfo: [CodingUserInfoKey: Any] { - return options.userInfo - } - - // The error context lenght - open var errorContextLenght: UInt = 0 - - // MARK: - Initialization - - /// Initializes `self` with the given top-level container and options. - init(referencing container: Box, at codingPath: [CodingKey] = [], options: XMLDecoder.Options) { - storage.push(container: container) - self.codingPath = codingPath - self.options = options - } - - // MARK: - Decoder Methods - - private func topContainer() throws -> Box { - guard let topContainer = storage.topContainer() else { - throw DecodingError.valueNotFound(Box.self, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Cannot get decoding container -- empty container stack." - )) - } - return topContainer - } - - private func popContainer() throws -> Box { - guard let topContainer = storage.popContainer() else { - throw DecodingError.valueNotFound(Box.self, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Cannot get decoding container -- empty container stack." - )) - } - return topContainer - } - - public func container(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { - let topContainer = try self.topContainer() - - guard !topContainer.isNull else { - throw DecodingError.valueNotFound(KeyedDecodingContainer.self, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Cannot get keyed decoding container -- found null box instead." - )) - } - - guard let keyed = topContainer as? SharedBox else { - throw DecodingError._typeMismatch( - at: codingPath, - expectation: [String: Any].self, - reality: topContainer - ) - } - - let container = XMLKeyedDecodingContainer(referencing: self, wrapping: keyed) - return KeyedDecodingContainer(container) - } - - public func unkeyedContainer() throws -> UnkeyedDecodingContainer { - let topContainer = try self.topContainer() - - guard !topContainer.isNull else { - throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Cannot get unkeyed decoding container -- found null box instead." - )) - } - - let unkeyed = (topContainer as? SharedBox) ?? SharedBox(UnkeyedBox([topContainer])) - - return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed) - } - - public func singleValueContainer() throws -> SingleValueDecodingContainer { - return self - } -} - -extension XMLDecoderImplementation: SingleValueDecodingContainer { - // MARK: SingleValueDecodingContainer Methods - - public func decodeNil() -> Bool { - return (try? topContainer().isNull) ?? true - } - - public func decode(_: Bool.Type) throws -> Bool { - return try unbox(try topContainer()) - } - - public func decode(_: Decimal.Type) throws -> Decimal { - return try unbox(try topContainer()) - } - - public func decode(_: T.Type) throws -> T { - return try unbox(try topContainer()) - } - - public func decode(_: T.Type) throws -> T { - return try unbox(try topContainer()) - } - - public func decode(_: T.Type) throws -> T { - return try unbox(try topContainer()) - } - - public func decode(_: String.Type) throws -> String { - return try unbox(try topContainer()) - } - - public func decode(_: String.Type) throws -> Date { - return try unbox(try topContainer()) - } - - public func decode(_: String.Type) throws -> Data { - return try unbox(try topContainer()) - } - - public func decode(_: T.Type) throws -> T { - return try unbox(try topContainer()) - } -} - -// MARK: - Concrete Value Representations - -extension XMLDecoderImplementation { - /// Returns the given box unboxed from a container. - - private func typedBox(_ box: Box, for valueType: T.Type) throws -> B { - guard let typedBox = box as? B else { - if box is NullBox { - throw DecodingError.valueNotFound(valueType, DecodingError.Context( - codingPath: codingPath, - debugDescription: "Expected \(valueType) but found null instead." - )) - } else { - throw DecodingError._typeMismatch(at: codingPath, expectation: valueType, reality: box) - } - } - - return typedBox - } - - func unbox(_ box: Box) throws -> Bool { - let stringBox: StringBox = try typedBox(box, for: Bool.self) - let string = stringBox.unbox() - - guard let boolBox = BoolBox(xmlString: string) else { - throw DecodingError._typeMismatch(at: codingPath, expectation: Bool.self, reality: box) - } - - return boolBox.unbox() - } - - func unbox(_ box: Box) throws -> Decimal { - let stringBox: StringBox = try typedBox(box, for: Decimal.self) - let string = stringBox.unbox() - - guard let decimalBox = DecimalBox(xmlString: string) else { - throw DecodingError._typeMismatch(at: codingPath, expectation: Decimal.self, reality: box) - } - - return decimalBox.unbox() - } - - func unbox(_ box: Box) throws -> T { - let stringBox: StringBox = try typedBox(box, for: T.self) - let string = stringBox.unbox() - - guard let intBox = IntBox(xmlString: string) else { - throw DecodingError._typeMismatch(at: codingPath, expectation: T.self, reality: box) - } - - guard let int: T = intBox.unbox() else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)." - )) - } - - return int - } - - func unbox(_ box: Box) throws -> T { - let stringBox: StringBox = try typedBox(box, for: T.self) - let string = stringBox.unbox() - - guard let uintBox = UIntBox(xmlString: string) else { - throw DecodingError._typeMismatch(at: codingPath, expectation: T.self, reality: box) - } - - guard let uint: T = uintBox.unbox() else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)." - )) - } - - return uint - } - - func unbox(_ box: Box) throws -> T { - let stringBox: StringBox = try typedBox(box, for: T.self) - let string = stringBox.unbox() - - guard let floatBox = FloatBox(xmlString: string) else { - throw DecodingError._typeMismatch(at: codingPath, expectation: T.self, reality: box) - } - - guard let float: T = floatBox.unbox() else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)." - )) - } - - return float - } - - func unbox(_ box: Box) throws -> String { - let stringBox: StringBox = try typedBox(box, for: String.self) - let string = stringBox.unbox() - - return string - } - - func unbox(_ box: Box) throws -> Date { - switch options.dateDecodingStrategy { - case .deferredToDate: - storage.push(container: box) - defer { storage.popContainer() } - return try Date(from: self) - - case .secondsSince1970: - let stringBox: StringBox = try typedBox(box, for: Date.self) - let string = stringBox.unbox() - - guard let dateBox = DateBox(secondsSince1970: string) else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Expected date string to be formatted in seconds since 1970." - )) - } - return dateBox.unbox() - case .millisecondsSince1970: - let stringBox: StringBox = try typedBox(box, for: Date.self) - let string = stringBox.unbox() - - guard let dateBox = DateBox(millisecondsSince1970: string) else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Expected date string to be formatted in milliseconds since 1970." - )) - } - return dateBox.unbox() - case .iso8601: - let stringBox: StringBox = try typedBox(box, for: Date.self) - let string = stringBox.unbox() - - guard let dateBox = DateBox(iso8601: string) else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Expected date string to be ISO8601-formatted." - )) - } - return dateBox.unbox() - case let .formatted(formatter): - let stringBox: StringBox = try typedBox(box, for: Date.self) - let string = stringBox.unbox() - - guard let dateBox = DateBox(xmlString: string, formatter: formatter) else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Date string does not match format expected by formatter." - )) - } - return dateBox.unbox() - case let .custom(closure): - storage.push(container: box) - defer { storage.popContainer() } - return try closure(self) - } - } - - func unbox(_ box: Box) throws -> Data { - switch options.dataDecodingStrategy { - case .deferredToData: - storage.push(container: box) - defer { storage.popContainer() } - return try Data(from: self) - case .base64: - let stringBox: StringBox = try typedBox(box, for: Data.self) - let string = stringBox.unbox() - - guard let dataBox = DataBox(base64: string) else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Encountered Data is not valid Base64" - )) - } - return dataBox.unbox() - case let .custom(closure): - storage.push(container: box) - defer { storage.popContainer() } - return try closure(self) - } - } - - func unbox(_ box: Box) throws -> URL { - let stringBox: StringBox = try typedBox(box, for: URL.self) - let string = stringBox.unbox() - - guard let urlBox = URLBox(xmlString: string) else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Encountered Data is not valid Base64" - )) - } - - return urlBox.unbox() - } - - private struct TypeMismatch: Error {} - - func unbox(_ box: Box) throws -> T { - let decoded: T? - let type = T.self - - if type == Date.self || type == NSDate.self { - let date: Date = try unbox(box) - - decoded = date as? T - } else if type == Data.self || type == NSData.self { - let data: Data = try unbox(box) - decoded = data as? T - } else if type == URL.self || type == NSURL.self { - let data: URL = try unbox(box) - decoded = data as? T - } else if type == Decimal.self || type == NSDecimalNumber.self { - let decimal: Decimal = try unbox(box) - decoded = decimal as? T - } else if - type == String.self || type == NSString.self, - let str: String = try? unbox(box), let value = str as? T { - decoded = value - } else { - storage.push(container: box) - decoded = try type.init(from: self) - storage.popContainer() - } - - guard let result = decoded else { - throw DecodingError._typeMismatch( - at: codingPath, expectation: type, reality: box - ) - } - - return result - } -} diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation+SingleValueDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation+SingleValueDecodingContainer.swift new file mode 100644 index 00000000..989da383 --- /dev/null +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation+SingleValueDecodingContainer.swift @@ -0,0 +1,53 @@ +// +// XMLDecoder.swift +// XMLCoder +// +// Created by Shawn Moore on 11/20/17. +// Copyright © 2017 Shawn Moore. All rights reserved. +// + +import Foundation + +extension XMLDecoderImplementation: SingleValueDecodingContainer { + // MARK: SingleValueDecodingContainer Methods + + public func decodeNil() -> Bool { + return (try? topContainer().isNull) ?? true + } + + public func decode(_: Bool.Type) throws -> Bool { + return try unbox(try topContainer()) + } + + public func decode(_: Decimal.Type) throws -> Decimal { + return try unbox(try topContainer()) + } + + public func decode(_: T.Type) throws -> T { + return try unbox(try topContainer()) + } + + public func decode(_: T.Type) throws -> T { + return try unbox(try topContainer()) + } + + public func decode(_: T.Type) throws -> T { + return try unbox(try topContainer()) + } + + public func decode(_: String.Type) throws -> String { + return try unbox(try topContainer()) + } + + public func decode(_: String.Type) throws -> Date { + return try unbox(try topContainer()) + } + + public func decode(_: String.Type) throws -> Data { + return try unbox(try topContainer()) + } + + public func decode(_: T.Type) throws -> T { + return try unbox(try topContainer()) + } +} diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift new file mode 100644 index 00000000..37e48534 --- /dev/null +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -0,0 +1,340 @@ +// +// XMLDecoder.swift +// XMLCoder +// +// Created by Shawn Moore on 11/20/17. +// Copyright © 2017 Shawn Moore. All rights reserved. +// + +import Foundation + +class XMLDecoderImplementation: Decoder { + // MARK: Properties + + /// The decoder's storage. + var storage: XMLDecodingStorage = XMLDecodingStorage() + + /// Options set on the top-level decoder. + let options: XMLDecoder.Options + + /// The path to the current point in encoding. + public internal(set) var codingPath: [CodingKey] + + /// Contextual user-provided information for use during encoding. + public var userInfo: [CodingUserInfoKey: Any] { + return options.userInfo + } + + // The error context lenght + open var errorContextLenght: UInt = 0 + + // MARK: - Initialization + + /// Initializes `self` with the given top-level container and options. + init(referencing container: Box, at codingPath: [CodingKey] = [], options: XMLDecoder.Options) { + storage.push(container: container) + self.codingPath = codingPath + self.options = options + } + + // MARK: - Decoder Methods + + internal func topContainer() throws -> Box { + guard let topContainer = storage.topContainer() else { + throw DecodingError.valueNotFound(Box.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get decoding container -- empty container stack." + )) + } + return topContainer + } + + private func popContainer() throws -> Box { + guard let topContainer = storage.popContainer() else { + throw DecodingError.valueNotFound(Box.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get decoding container -- empty container stack." + )) + } + return topContainer + } + + public func container(keyedBy _: Key.Type) throws -> KeyedDecodingContainer { + let topContainer = try self.topContainer() + + guard !topContainer.isNull else { + throw DecodingError.valueNotFound(KeyedDecodingContainer.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get keyed decoding container -- found null box instead." + )) + } + + guard let keyed = topContainer as? SharedBox else { + throw DecodingError._typeMismatch( + at: codingPath, + expectation: [String: Any].self, + reality: topContainer + ) + } + + let container = XMLKeyedDecodingContainer(referencing: self, wrapping: keyed) + return KeyedDecodingContainer(container) + } + + public func unkeyedContainer() throws -> UnkeyedDecodingContainer { + let topContainer = try self.topContainer() + + guard !topContainer.isNull else { + throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Cannot get unkeyed decoding container -- found null box instead." + )) + } + + let unkeyed = (topContainer as? SharedBox) ?? SharedBox(UnkeyedBox([topContainer])) + + return XMLUnkeyedDecodingContainer(referencing: self, wrapping: unkeyed) + } + + public func singleValueContainer() throws -> SingleValueDecodingContainer { + return self + } +} + +// MARK: - Concrete Value Representations + +extension XMLDecoderImplementation { + /// Returns the given box unboxed from a container. + + private func typedBox(_ box: Box, for valueType: T.Type) throws -> B { + guard let typedBox = box as? B else { + if box is NullBox { + throw DecodingError.valueNotFound(valueType, DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected \(valueType) but found null instead." + )) + } else { + throw DecodingError._typeMismatch(at: codingPath, expectation: valueType, reality: box) + } + } + + return typedBox + } + + func unbox(_ box: Box) throws -> Bool { + let stringBox: StringBox = try typedBox(box, for: Bool.self) + let string = stringBox.unbox() + + guard let boolBox = BoolBox(xmlString: string) else { + throw DecodingError._typeMismatch(at: codingPath, expectation: Bool.self, reality: box) + } + + return boolBox.unbox() + } + + func unbox(_ box: Box) throws -> Decimal { + let stringBox: StringBox = try typedBox(box, for: Decimal.self) + let string = stringBox.unbox() + + guard let decimalBox = DecimalBox(xmlString: string) else { + throw DecodingError._typeMismatch(at: codingPath, expectation: Decimal.self, reality: box) + } + + return decimalBox.unbox() + } + + func unbox(_ box: Box) throws -> T { + let stringBox: StringBox = try typedBox(box, for: T.self) + let string = stringBox.unbox() + + guard let intBox = IntBox(xmlString: string) else { + throw DecodingError._typeMismatch(at: codingPath, expectation: T.self, reality: box) + } + + guard let int: T = intBox.unbox() else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)." + )) + } + + return int + } + + func unbox(_ box: Box) throws -> T { + let stringBox: StringBox = try typedBox(box, for: T.self) + let string = stringBox.unbox() + + guard let uintBox = UIntBox(xmlString: string) else { + throw DecodingError._typeMismatch(at: codingPath, expectation: T.self, reality: box) + } + + guard let uint: T = uintBox.unbox() else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)." + )) + } + + return uint + } + + func unbox(_ box: Box) throws -> T { + let stringBox: StringBox = try typedBox(box, for: T.self) + let string = stringBox.unbox() + + guard let floatBox = FloatBox(xmlString: string) else { + throw DecodingError._typeMismatch(at: codingPath, expectation: T.self, reality: box) + } + + guard let float: T = floatBox.unbox() else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Parsed XML number <\(string)> does not fit in \(T.self)." + )) + } + + return float + } + + func unbox(_ box: Box) throws -> String { + let stringBox: StringBox = try typedBox(box, for: String.self) + let string = stringBox.unbox() + + return string + } + + func unbox(_ box: Box) throws -> Date { + switch options.dateDecodingStrategy { + case .deferredToDate: + storage.push(container: box) + defer { storage.popContainer() } + return try Date(from: self) + + case .secondsSince1970: + let stringBox: StringBox = try typedBox(box, for: Date.self) + let string = stringBox.unbox() + + guard let dateBox = DateBox(secondsSince1970: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected date string to be formatted in seconds since 1970." + )) + } + return dateBox.unbox() + case .millisecondsSince1970: + let stringBox: StringBox = try typedBox(box, for: Date.self) + let string = stringBox.unbox() + + guard let dateBox = DateBox(millisecondsSince1970: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected date string to be formatted in milliseconds since 1970." + )) + } + return dateBox.unbox() + case .iso8601: + let stringBox: StringBox = try typedBox(box, for: Date.self) + let string = stringBox.unbox() + + guard let dateBox = DateBox(iso8601: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Expected date string to be ISO8601-formatted." + )) + } + return dateBox.unbox() + case let .formatted(formatter): + let stringBox: StringBox = try typedBox(box, for: Date.self) + let string = stringBox.unbox() + + guard let dateBox = DateBox(xmlString: string, formatter: formatter) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Date string does not match format expected by formatter." + )) + } + return dateBox.unbox() + case let .custom(closure): + storage.push(container: box) + defer { storage.popContainer() } + return try closure(self) + } + } + + func unbox(_ box: Box) throws -> Data { + switch options.dataDecodingStrategy { + case .deferredToData: + storage.push(container: box) + defer { storage.popContainer() } + return try Data(from: self) + case .base64: + let stringBox: StringBox = try typedBox(box, for: Data.self) + let string = stringBox.unbox() + + guard let dataBox = DataBox(base64: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Encountered Data is not valid Base64" + )) + } + return dataBox.unbox() + case let .custom(closure): + storage.push(container: box) + defer { storage.popContainer() } + return try closure(self) + } + } + + func unbox(_ box: Box) throws -> URL { + let stringBox: StringBox = try typedBox(box, for: URL.self) + let string = stringBox.unbox() + + guard let urlBox = URLBox(xmlString: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Encountered Data is not valid Base64" + )) + } + + return urlBox.unbox() + } + + private struct TypeMismatch: Error {} + + func unbox(_ box: Box) throws -> T { + let decoded: T? + let type = T.self + + if type == Date.self || type == NSDate.self { + let date: Date = try unbox(box) + + decoded = date as? T + } else if type == Data.self || type == NSData.self { + let data: Data = try unbox(box) + decoded = data as? T + } else if type == URL.self || type == NSURL.self { + let data: URL = try unbox(box) + decoded = data as? T + } else if type == Decimal.self || type == NSDecimalNumber.self { + let decimal: Decimal = try unbox(box) + decoded = decimal as? T + } else if + type == String.self || type == NSString.self, + let str: String = try? unbox(box), let value = str as? T { + decoded = value + } else { + storage.push(container: box) + decoded = try type.init(from: self) + storage.popContainer() + } + + guard let result = decoded else { + throw DecodingError._typeMismatch( + at: codingPath, expectation: type, reality: box + ) + } + + return result + } +} diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index 820b1d8d..d06dc5b9 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -342,311 +342,3 @@ open class XMLEncoder { .data(using: .utf8, allowLossyConversion: true)! } } - -class XMLEncoderImplementation: Encoder { - // MARK: Properties - - /// The encoder's storage. - var storage: XMLEncodingStorage - - /// Options set on the top-level encoder. - let options: XMLEncoder.Options - - /// The path to the current point in encoding. - public var codingPath: [CodingKey] - - public var nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding] - - /// Contextual user-provided information for use during encoding. - public var userInfo: [CodingUserInfoKey: Any] { - return options.userInfo - } - - // MARK: - Initialization - - /// Initializes `self` with the given top-level encoder options. - init( - options: XMLEncoder.Options, - nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding], - codingPath: [CodingKey] = [] - ) { - self.options = options - storage = XMLEncodingStorage() - self.codingPath = codingPath - self.nodeEncodings = nodeEncodings - } - - /// Returns whether a new element can be encoded at this coding path. - /// - /// `true` if an element has not yet been encoded at this coding path; `false` otherwise. - var canEncodeNewValue: Bool { - // Every time a new value gets encoded, the key it's encoded for is - // pushed onto the coding path (even if it's a nil key from an unkeyed container). - // At the same time, every time a container is requested, a new value - // gets pushed onto the storage stack. - // If there are more values on the storage stack than on the coding path, - // it means the value is requesting more than one container, which - // violates the precondition. - // - // This means that anytime something that can request a new container - // goes onto the stack, we MUST push a key onto the coding path. - // Things which will not request containers do not need to have the - // coding path extended for them (but it doesn't matter if it is, - // because they will not reach here). - return storage.count == codingPath.count - } - - // MARK: - Encoder Methods - - public func container(keyedBy _: Key.Type) -> KeyedEncodingContainer { - // If an existing keyed container was already requested, return that one. - let topContainer: SharedBox - if canEncodeNewValue { - // We haven't yet pushed a container at this level; do so here. - topContainer = storage.pushKeyedContainer() - } else { - guard let container = storage.lastContainer as? SharedBox else { - preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.") - } - - topContainer = container - } - - let container = XMLKeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) - return KeyedEncodingContainer(container) - } - - public func unkeyedContainer() -> UnkeyedEncodingContainer { - // If an existing unkeyed container was already requested, return that one. - let topContainer: SharedBox - if canEncodeNewValue { - // We haven't yet pushed a container at this level; do so here. - topContainer = storage.pushUnkeyedContainer() - } else { - guard let container = storage.lastContainer as? SharedBox else { - preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.") - } - - topContainer = container - } - - return XMLUnkeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) - } - - public func singleValueContainer() -> SingleValueEncodingContainer { - return self - } -} - -extension XMLEncoderImplementation: SingleValueEncodingContainer { - // MARK: - SingleValueEncodingContainer Methods - - func assertCanEncodeNewValue() { - precondition(canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.") - } - - public func encodeNil() throws { - assertCanEncodeNewValue() - storage.push(container: box()) - } - - public func encode(_ value: Bool) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int8) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int16) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int32) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Int64) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt8) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt16) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt32) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: UInt64) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: String) throws { - assertCanEncodeNewValue() - storage.push(container: box(value)) - } - - public func encode(_ value: Float) throws { - assertCanEncodeNewValue() - try storage.push(container: box(value)) - } - - public func encode(_ value: Double) throws { - assertCanEncodeNewValue() - try storage.push(container: box(value)) - } - - public func encode(_ value: T) throws { - assertCanEncodeNewValue() - try storage.push(container: box(value)) - } -} - -extension XMLEncoderImplementation { - /// Returns the given value boxed in a container appropriate for pushing onto the container stack. - func box() -> SimpleBox { - return NullBox() - } - - func box(_ value: Bool) -> SimpleBox { - return BoolBox(value) - } - - func box(_ value: Decimal) -> SimpleBox { - return DecimalBox(value) - } - - func box(_ value: T) -> SimpleBox { - return IntBox(value) - } - - func box(_ value: T) -> SimpleBox { - return UIntBox(value) - } - - func box(_ value: T) throws -> SimpleBox { - guard value.isInfinite || value.isNaN else { - return FloatBox(value) - } - guard case let .convertToString(positiveInfinity: posInfString, - negativeInfinity: negInfString, - nan: nanString) = options.nonConformingFloatEncodingStrategy else { - throw EncodingError._invalidFloatingPointValue(value, at: codingPath) - } - if value == T.infinity { - return StringBox(posInfString) - } else if value == -T.infinity { - return StringBox(negInfString) - } else { - return StringBox(nanString) - } - } - - func box(_ value: String) -> SimpleBox { - return StringBox(value) - } - - func box(_ value: Date) throws -> Box { - switch options.dateEncodingStrategy { - case .deferredToDate: - try value.encode(to: self) - return storage.popContainer() - case .secondsSince1970: - return DateBox(value, format: .secondsSince1970) - case .millisecondsSince1970: - return DateBox(value, format: .millisecondsSince1970) - case .iso8601: - return DateBox(value, format: .iso8601) - case let .formatted(formatter): - return DateBox(value, format: .formatter(formatter)) - case let .custom(closure): - let depth = storage.count - try closure(value, self) - - guard storage.count > depth else { - return KeyedBox() - } - - return storage.popContainer() - } - } - - func box(_ value: Data) throws -> Box { - switch options.dataEncodingStrategy { - case .deferredToData: - try value.encode(to: self) - return storage.popContainer() - case .base64: - return DataBox(value, format: .base64) - case let .custom(closure): - let depth = storage.count - try closure(value, self) - - guard storage.count > depth else { - return KeyedBox() - } - - return storage.popContainer() - } - } - - func box(_ value: URL) -> SimpleBox { - return URLBox(value) - } - - func box(_ value: T) throws -> Box { - if T.self == Date.self || T.self == NSDate.self, - let value = value as? Date { - return try box(value) - } else if T.self == Data.self || T.self == NSData.self, - let value = value as? Data { - return try box(value) - } else if T.self == URL.self || T.self == NSURL.self, - let value = value as? URL { - return box(value) - } else if T.self == Decimal.self || T.self == NSDecimalNumber.self, - let value = value as? Decimal { - return box(value) - } - - let depth = storage.count - try value.encode(to: self) - - // The top container should be a new container. - guard storage.count > depth else { - return KeyedBox() - } - - let lastContainer = storage.popContainer() - - guard let sharedBox = lastContainer as? TypeErasedSharedBoxProtocol else { - return lastContainer - } - - return sharedBox.typeErasedUnbox() - } -} diff --git a/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift b/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift new file mode 100644 index 00000000..a10a4fe2 --- /dev/null +++ b/Sources/XMLCoder/Encoder/XMLEncoderImplementation+SingleValueEncodingContainer.swift @@ -0,0 +1,97 @@ +// +// XMLEncoder.swift +// XMLCoder +// +// Created by Shawn Moore on 11/22/17. +// Copyright © 2017 Shawn Moore. All rights reserved. +// + +import Foundation + +extension XMLEncoderImplementation: SingleValueEncodingContainer { + // MARK: - SingleValueEncodingContainer Methods + + func assertCanEncodeNewValue() { + precondition(canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.") + } + + public func encodeNil() throws { + assertCanEncodeNewValue() + storage.push(container: box()) + } + + public func encode(_ value: Bool) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int8) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int16) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int32) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int64) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt8) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt16) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt32) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt64) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: String) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Float) throws { + assertCanEncodeNewValue() + try storage.push(container: box(value)) + } + + public func encode(_ value: Double) throws { + assertCanEncodeNewValue() + try storage.push(container: box(value)) + } + + public func encode(_ value: T) throws { + assertCanEncodeNewValue() + try storage.push(container: box(value)) + } +} diff --git a/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift b/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift new file mode 100644 index 00000000..dd009547 --- /dev/null +++ b/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift @@ -0,0 +1,229 @@ +// +// XMLEncoder.swift +// XMLCoder +// +// Created by Shawn Moore on 11/22/17. +// Copyright © 2017 Shawn Moore. All rights reserved. +// + +import Foundation + +class XMLEncoderImplementation: Encoder { + // MARK: Properties + + /// The encoder's storage. + var storage: XMLEncodingStorage + + /// Options set on the top-level encoder. + let options: XMLEncoder.Options + + /// The path to the current point in encoding. + public var codingPath: [CodingKey] + + public var nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding] + + /// Contextual user-provided information for use during encoding. + public var userInfo: [CodingUserInfoKey: Any] { + return options.userInfo + } + + // MARK: - Initialization + + /// Initializes `self` with the given top-level encoder options. + init( + options: XMLEncoder.Options, + nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding], + codingPath: [CodingKey] = [] + ) { + self.options = options + storage = XMLEncodingStorage() + self.codingPath = codingPath + self.nodeEncodings = nodeEncodings + } + + /// Returns whether a new element can be encoded at this coding path. + /// + /// `true` if an element has not yet been encoded at this coding path; `false` otherwise. + var canEncodeNewValue: Bool { + // Every time a new value gets encoded, the key it's encoded for is + // pushed onto the coding path (even if it's a nil key from an unkeyed container). + // At the same time, every time a container is requested, a new value + // gets pushed onto the storage stack. + // If there are more values on the storage stack than on the coding path, + // it means the value is requesting more than one container, which + // violates the precondition. + // + // This means that anytime something that can request a new container + // goes onto the stack, we MUST push a key onto the coding path. + // Things which will not request containers do not need to have the + // coding path extended for them (but it doesn't matter if it is, + // because they will not reach here). + return storage.count == codingPath.count + } + + // MARK: - Encoder Methods + + public func container(keyedBy _: Key.Type) -> KeyedEncodingContainer { + // If an existing keyed container was already requested, return that one. + let topContainer: SharedBox + if canEncodeNewValue { + // We haven't yet pushed a container at this level; do so here. + topContainer = storage.pushKeyedContainer() + } else { + guard let container = storage.lastContainer as? SharedBox else { + preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.") + } + + topContainer = container + } + + let container = XMLKeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) + return KeyedEncodingContainer(container) + } + + public func unkeyedContainer() -> UnkeyedEncodingContainer { + // If an existing unkeyed container was already requested, return that one. + let topContainer: SharedBox + if canEncodeNewValue { + // We haven't yet pushed a container at this level; do so here. + topContainer = storage.pushUnkeyedContainer() + } else { + guard let container = storage.lastContainer as? SharedBox else { + preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.") + } + + topContainer = container + } + + return XMLUnkeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) + } + + public func singleValueContainer() -> SingleValueEncodingContainer { + return self + } +} + +extension XMLEncoderImplementation { + /// Returns the given value boxed in a container appropriate for pushing onto the container stack. + func box() -> SimpleBox { + return NullBox() + } + + func box(_ value: Bool) -> SimpleBox { + return BoolBox(value) + } + + func box(_ value: Decimal) -> SimpleBox { + return DecimalBox(value) + } + + func box(_ value: T) -> SimpleBox { + return IntBox(value) + } + + func box(_ value: T) -> SimpleBox { + return UIntBox(value) + } + + func box(_ value: T) throws -> SimpleBox { + guard value.isInfinite || value.isNaN else { + return FloatBox(value) + } + guard case let .convertToString(positiveInfinity: posInfString, + negativeInfinity: negInfString, + nan: nanString) = options.nonConformingFloatEncodingStrategy else { + throw EncodingError._invalidFloatingPointValue(value, at: codingPath) + } + if value == T.infinity { + return StringBox(posInfString) + } else if value == -T.infinity { + return StringBox(negInfString) + } else { + return StringBox(nanString) + } + } + + func box(_ value: String) -> SimpleBox { + return StringBox(value) + } + + func box(_ value: Date) throws -> Box { + switch options.dateEncodingStrategy { + case .deferredToDate: + try value.encode(to: self) + return storage.popContainer() + case .secondsSince1970: + return DateBox(value, format: .secondsSince1970) + case .millisecondsSince1970: + return DateBox(value, format: .millisecondsSince1970) + case .iso8601: + return DateBox(value, format: .iso8601) + case let .formatted(formatter): + return DateBox(value, format: .formatter(formatter)) + case let .custom(closure): + let depth = storage.count + try closure(value, self) + + guard storage.count > depth else { + return KeyedBox() + } + + return storage.popContainer() + } + } + + func box(_ value: Data) throws -> Box { + switch options.dataEncodingStrategy { + case .deferredToData: + try value.encode(to: self) + return storage.popContainer() + case .base64: + return DataBox(value, format: .base64) + case let .custom(closure): + let depth = storage.count + try closure(value, self) + + guard storage.count > depth else { + return KeyedBox() + } + + return storage.popContainer() + } + } + + func box(_ value: URL) -> SimpleBox { + return URLBox(value) + } + + func box(_ value: T) throws -> Box { + if T.self == Date.self || T.self == NSDate.self, + let value = value as? Date { + return try box(value) + } else if T.self == Data.self || T.self == NSData.self, + let value = value as? Data { + return try box(value) + } else if T.self == URL.self || T.self == NSURL.self, + let value = value as? URL { + return box(value) + } else if T.self == Decimal.self || T.self == NSDecimalNumber.self, + let value = value as? Decimal { + return box(value) + } + + let depth = storage.count + try value.encode(to: self) + + // The top container should be a new container. + guard storage.count > depth else { + return KeyedBox() + } + + let lastContainer = storage.popContainer() + + guard let sharedBox = lastContainer as? TypeErasedSharedBoxProtocol else { + return lastContainer + } + + return sharedBox.typeErasedUnbox() + } +} diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 1bd98f4f..42ceec1f 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -26,6 +26,10 @@ A61FE03C21E4EAB10015D993 /* KeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */; }; B35157CE21F986DD009CA0CC /* DynamicNodeEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */; }; B3BE1D612202C1F600259831 /* DynamicNodeEncodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */; }; + B3BE1D632202CB1400259831 /* XMLEncoderImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */; }; + B3BE1D652202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D642202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift */; }; + B3BE1D682202CBF800259831 /* XMLDecoderImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D662202CBF800259831 /* XMLDecoderImplementation.swift */; }; + B3BE1D692202CBF800259831 /* XMLDecoderImplementation+SingleValueDecodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D672202CBF800259831 /* XMLDecoderImplementation+SingleValueDecodingContainer.swift */; }; BF63EF0021CCDED2001D38C5 /* XMLStackParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63EEFF21CCDED2001D38C5 /* XMLStackParserTests.swift */; }; BF63EF0621CD7A74001D38C5 /* URLBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63EF0521CD7A74001D38C5 /* URLBox.swift */; }; BF63EF0821CD7AF8001D38C5 /* URLBoxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63EF0721CD7AF8001D38C5 /* URLBoxTests.swift */; }; @@ -130,6 +134,10 @@ A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = ""; }; B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncoding.swift; sourceTree = ""; }; B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncodingTest.swift; sourceTree = ""; }; + B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLEncoderImplementation.swift; sourceTree = ""; }; + B3BE1D642202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XMLEncoderImplementation+SingleValueEncodingContainer.swift"; sourceTree = ""; }; + B3BE1D662202CBF800259831 /* XMLDecoderImplementation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLDecoderImplementation.swift; sourceTree = ""; }; + B3BE1D672202CBF800259831 /* XMLDecoderImplementation+SingleValueDecodingContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XMLDecoderImplementation+SingleValueDecodingContainer.swift"; sourceTree = ""; }; BF63EEFF21CCDED2001D38C5 /* XMLStackParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLStackParserTests.swift; sourceTree = ""; }; BF63EF0521CD7A74001D38C5 /* URLBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBox.swift; sourceTree = ""; }; BF63EF0721CD7AF8001D38C5 /* URLBoxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBoxTests.swift; sourceTree = ""; }; @@ -329,6 +337,8 @@ children = ( OBJ_16 /* EncodingErrorExtension.swift */, OBJ_17 /* XMLEncoder.swift */, + B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */, + B3BE1D642202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift */, OBJ_18 /* XMLEncodingStorage.swift */, OBJ_19 /* XMLKeyedEncodingContainer.swift */, OBJ_20 /* XMLReferencingEncoder.swift */, @@ -421,6 +431,8 @@ children = ( OBJ_10 /* DecodingErrorExtension.swift */, OBJ_11 /* XMLDecoder.swift */, + B3BE1D662202CBF800259831 /* XMLDecoderImplementation.swift */, + B3BE1D672202CBF800259831 /* XMLDecoderImplementation+SingleValueDecodingContainer.swift */, OBJ_12 /* XMLDecodingStorage.swift */, OBJ_13 /* XMLKeyedDecodingContainer.swift */, OBJ_14 /* XMLUnkeyedDecodingContainer.swift */, @@ -540,15 +552,19 @@ isa = PBXSourcesBuildPhase; buildActionMask = 0; files = ( + B3BE1D632202CB1400259831 /* XMLEncoderImplementation.swift in Sources */, BF9457D621CBB59E005ACFDE /* FloatBox.swift in Sources */, BF9457B721CBB4DB005ACFDE /* XMLHeader.swift in Sources */, + B3BE1D692202CBF800259831 /* XMLDecoderImplementation+SingleValueDecodingContainer.swift in Sources */, BF9457BB21CBB4DB005ACFDE /* XMLKey.swift in Sources */, OBJ_48 /* DecodingErrorExtension.swift in Sources */, BF63EF1821CEB6BD001D38C5 /* SharedBox.swift in Sources */, + B3BE1D682202CBF800259831 /* XMLDecoderImplementation.swift in Sources */, BF9457DB21CBB5D2005ACFDE /* DateBox.swift in Sources */, BF63EF0621CD7A74001D38C5 /* URLBox.swift in Sources */, OBJ_49 /* XMLDecoder.swift in Sources */, OBJ_50 /* XMLDecodingStorage.swift in Sources */, + B3BE1D652202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift in Sources */, BF9457D521CBB59E005ACFDE /* UIntBox.swift in Sources */, OBJ_51 /* XMLKeyedDecodingContainer.swift in Sources */, OBJ_52 /* XMLUnkeyedDecodingContainer.swift in Sources */, From f5f6f8d200f82847e32e305ddd20c1ff079f80c2 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 03:50:37 -0500 Subject: [PATCH 10/24] Rename left over values from different branch --- Tests/XMLCoderTests/DynamicNodeEncodingTest.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift index 0e74471a..558fbdaa 100644 --- a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift +++ b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift @@ -1,5 +1,5 @@ // -// SOAPSample.swift +// DynamicNodeEncodingTest.swift // XMLCoderTests // // Created by Joseph Mattiello on 1/23/19. @@ -121,8 +121,7 @@ private func decodeArray(_ decoder: Decoder, decode: (inout UnkeyedDecodingCo return decoded } -final class IntrinsicTest: XCTestCase { - +final class DynamicNodeEncodingTest: XCTestCase { func testEncode() { let book1 = Book(id: 123, title: "Cat in the Hat", From e86be143c43550ec8b18a10ef89af7ab8edac115 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 05:00:01 -0500 Subject: [PATCH 11/24] test: Add coding / decoding tests to DynamicNodeEncoding Had removed them since I was testing intrinsics with attributes. Needed to wrap cateogy value in an element tag again. Also appears the hack for decoding nested arrays is no longer required so removed the complex decoding of Category --- .../DynamicNodeEncodingTest.swift | 141 +++++++++++------- 1 file changed, 90 insertions(+), 51 deletions(-) diff --git a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift index 558fbdaa..845a0d4a 100644 --- a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift +++ b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift @@ -15,14 +15,14 @@ let libraryXML = """ 123 Cat in the Hat - Kids - Wildlife + Kids + Wildlife 789 1984 - Classics - News + Classics + News """.data(using: .utf8)! @@ -56,39 +56,13 @@ private struct Book: Codable, Equatable, DynamicNodeEncoding { } } -extension Book { - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(UInt.self, forKey: .id) - title = try container.decode(String.self, forKey: .title) - - var nested = try container.nestedUnkeyedContainer(forKey: .categories) - - var decoded = [Category]() - var finished = false - - while !finished { - do { - let another = try nested.decode(Category.self) - decoded.append(another) - } catch DecodingError.valueNotFound { - finished = true - } catch { - throw error - } - } - - categories = decoded - } -} - private struct Category: Codable, Equatable, DynamicNodeEncoding { let main: Bool let value: String private enum CodingKeys: String, CodingKey { case main - case value = "" + case value } static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { @@ -101,26 +75,6 @@ private struct Category: Codable, Equatable, DynamicNodeEncoding { } } -private func decodeArray(_ decoder: Decoder, decode: (inout UnkeyedDecodingContainer) throws -> T) throws -> [T] { - let keyedContainer = try decoder.container(keyedBy: CodingKeys.self) - var container = try keyedContainer.nestedUnkeyedContainer(forKey: .value) - - var decoded = [T]() - var finished = false - - while !finished { - do { - decoded.append(try decode(&container)) - } catch DecodingError.valueNotFound { - finished = true - } catch { - throw error - } - } - - return decoded -} - final class DynamicNodeEncodingTest: XCTestCase { func testEncode() { let book1 = Book(id: 123, @@ -153,7 +107,92 @@ final class DynamicNodeEncodingTest: XCTestCase { } } + func testDecode() { + do { + + let decoder = XMLDecoder() + decoder.errorContextLength = 10 + + let library = try decoder.decode(Library.self, from: libraryXML) + 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) + } catch { + print("Test threw error: " + error.localizedDescription) + XCTFail(error.localizedDescription) + } + } + + func testEncodeDecode() { + do { + let decoder = XMLDecoder() + decoder.errorContextLength = 10 + + let encoder = XMLEncoder() + encoder.outputFormatting = [.prettyPrinted] + + let library = try decoder.decode(Library.self, from: libraryXML) + 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) + + let data = try encoder.encode(library, withRootKey: "library", + header: XMLHeader(version: 1.0, + encoding: "UTF-8")) + print(String(data: data, encoding: .utf8)!) + let library2 = try decoder.decode(Library.self, from: data) + XCTAssertEqual(library, library2) + } catch { + print("Test threw error: " + error.localizedDescription) + XCTFail(error.localizedDescription) + } + } + static var allTests = [ ("testEncode", testEncode), + ("testDecode", testDecode), + ("testEncodeDecode", testEncodeDecode), ] } From 5e945575707a62c46e87d24e0874dd1529181f19 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 05:01:53 -0500 Subject: [PATCH 12/24] Convrted BooksTest to DynamicNodeEncoding, tests string equality Previous version of this test techncially passed on Encdode/Decode comparision sinve the structure values were the same, but the encoding make Book structs id an element, so the strings weren't equal. Modified the simplier single book test to check that the attributes are encoded to XML and match the original string (minus white space formatting) --- Tests/XMLCoderTests/BooksTest.swift | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/Tests/XMLCoderTests/BooksTest.swift b/Tests/XMLCoderTests/BooksTest.swift index d8e6c8d9..de1b5785 100644 --- a/Tests/XMLCoderTests/BooksTest.swift +++ b/Tests/XMLCoderTests/BooksTest.swift @@ -11,15 +11,15 @@ import XCTest @testable import XMLCoder private let bookXML = """ - + Gambardella, Matthew - XML Developer's Guide + An in-depth look at creating applications + with XML. Computer 44.95 2000-10-01 - An in-depth look at creating applications - with XML. + XML Developer's Guide """.data(using: .utf8)! @@ -155,7 +155,7 @@ private struct Catalog: Codable, Equatable { } } -private struct Book: Codable, Equatable { +private struct Book: Codable, Equatable, DynamicNodeEncoding { var id: String var author: String var title: String @@ -169,6 +169,15 @@ private struct Book: Codable, Equatable { case publishDate = "publish_date" } + + static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.id: + return .attribute + default: + return .element + } + } } private enum Genre: String, Codable { @@ -191,6 +200,8 @@ final class BooksTest: XCTestCase { let decoder = XMLDecoder() let encoder = XMLEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + decoder.dateDecodingStrategy = .formatted(formatter) encoder.dateEncodingStrategy = .formatted(formatter) @@ -198,11 +209,19 @@ final class BooksTest: XCTestCase { XCTAssertEqual(book1.publishDate, Date(timeIntervalSince1970: 970_358_400)) + XCTAssertEqual(book1.title, "XML Developer's Guide") + let data = try encoder.encode(book1, withRootKey: "book", header: XMLHeader(version: 1.0, encoding: "UTF-8")) let book2 = try decoder.decode(Book.self, from: data) + XCTAssertEqual(book1, book2) + + // Test string equivlancy + let encodedXML = String(data: data, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + let originalXML = String(data: bookXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(encodedXML, originalXML) } func testCatalogXML() throws { From 39b599994033ae9b5e3f171f9c4d9e91202121ce Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 14:01:12 -0500 Subject: [PATCH 13/24] Swiftfomat corrections --- Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift | 6 +++--- .../XMLCoder/Auxiliaries/String+Extensions.swift | 12 ++++++------ .../XMLCoder/Auxiliaries/XMLCoderElement.swift | 5 +++-- Sources/XMLCoder/Encoder/XMLEncoder.swift | 6 +++--- .../Encoder/XMLEncoderImplementation.swift | 4 ++-- .../Encoder/XMLKeyedEncodingContainer.swift | 2 +- Tests/XMLCoderTests/DynamicNodeEncodingTest.swift | 15 +++++++-------- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift index a779e0f8..9d572993 100644 --- a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift +++ b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift @@ -84,9 +84,9 @@ struct KeyedBox { extension KeyedBox { init(elements: E, attributes: A) where E: Sequence, E.Element == (Key, Element), A: Sequence, A.Element == (Key, Attribute) { - let elements = Elements(Dictionary(uniqueKeysWithValues: elements)) - let attributes = Attributes(Dictionary(uniqueKeysWithValues: attributes)) - self.init(elements: elements, attributes: attributes) + let elements = Elements(Dictionary(uniqueKeysWithValues: elements)) + let attributes = Attributes(Dictionary(uniqueKeysWithValues: attributes)) + self.init(elements: elements, attributes: attributes) } init(elements: [Key: Element], attributes: [Key: Attribute]) { diff --git a/Sources/XMLCoder/Auxiliaries/String+Extensions.swift b/Sources/XMLCoder/Auxiliaries/String+Extensions.swift index 867637ca..1dc45c6c 100644 --- a/Sources/XMLCoder/Auxiliaries/String+Extensions.swift +++ b/Sources/XMLCoder/Auxiliaries/String+Extensions.swift @@ -7,7 +7,7 @@ import Foundation -extension StringProtocol where Self.Index == String.Index { +extension StringProtocol where Self.Index == String.Index { func escape(_ characterSet: [(character: String, escapedCharacter: String)]) -> String { var string = String(self) @@ -24,22 +24,22 @@ extension StringProtocol { guard count > 1 else { return self } - return Self(prefix(1).uppercased() + self.dropFirst())! + return Self(prefix(1).uppercased() + dropFirst())! } mutating func capitalizeFirstLetter() { - self = self.capitalizingFirstLetter() + self = capitalizingFirstLetter() } func lowercasingFirstLetter() -> Self { // avoid lowercasing single letters (I), or capitalized multiples (AThing ! to aThing, leave as AThing) - guard count > 1 && !(String(prefix(2)) == prefix(2).uppercased()) else { + guard count > 1, !(String(prefix(2)) == prefix(2).uppercased()) else { return self } - return Self(prefix(1).lowercased() + self.dropFirst())! + return Self(prefix(1).lowercased() + dropFirst())! } mutating func lowercaseFirstLetter() { - self = self.capitalizingFirstLetter() + self = capitalizingFirstLetter() } } diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 0a685acd..fc1f5cd6 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -27,7 +27,7 @@ struct XMLCoderElement: Equatable { value: String? = nil, elements: [XMLCoderElement] = [], attributes: [String: String] = [:] - ) { + ) { self.key = key self.value = value self.elements = elements @@ -47,7 +47,7 @@ struct XMLCoderElement: Equatable { func flatten() -> KeyedBox { let attributes = self.attributes.mapValues { StringBox($0) } - let keyedElements: [String: Box] = self.elements.reduce([String: Box]()) { (result, element) -> [String: Box] in + let keyedElements: [String: Box] = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in var result = result let key = element.key @@ -274,6 +274,7 @@ struct XMLCoderElement: Equatable { } // MARK: - Convenience Initializers + extension XMLCoderElement { init(key: String, box: UnkeyedBox) { let elements = box.map { box in diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index d06dc5b9..708f5495 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -229,7 +229,7 @@ open class XMLEncoder { func nodeEncodings(forType codableType: Encodable.Type, with encoder: Encoder) -> ((CodingKey) -> XMLEncoder.NodeEncoding) { - return self.encoderClosure(codableType, encoder) + return encoderClosure(codableType, encoder) } var encoderClosure: XMLEncodingClosure { @@ -239,9 +239,9 @@ open class XMLEncoder { } } - static let defaultEncoder: XMLEncodingClosure = { codableType, encoder in + static let defaultEncoder: XMLEncodingClosure = { codableType, _ in guard let dynamicType = codableType as? DynamicNodeEncoding.Type else { - return { _ in return .default } + return { _ in .default } } return dynamicType.nodeEncoding(forKey:) } diff --git a/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift b/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift index dd009547..e0e710ee 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoderImplementation.swift @@ -34,7 +34,7 @@ class XMLEncoderImplementation: Encoder { options: XMLEncoder.Options, nodeEncodings: [(CodingKey) -> XMLEncoder.NodeEncoding], codingPath: [CodingKey] = [] - ) { + ) { self.options = options storage = XMLEncodingStorage() self.codingPath = codingPath @@ -132,7 +132,7 @@ extension XMLEncoderImplementation { guard case let .convertToString(positiveInfinity: posInfString, negativeInfinity: negInfString, nan: nanString) = options.nonConformingFloatEncodingStrategy else { - throw EncodingError._invalidFloatingPointValue(value, at: codingPath) + throw EncodingError._invalidFloatingPointValue(value, at: codingPath) } if value == T.infinity { return StringBox(posInfString) diff --git a/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift b/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift index a5229898..f0e0a3b0 100644 --- a/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift +++ b/Sources/XMLCoder/Encoder/XMLKeyedEncodingContainer.swift @@ -113,7 +113,7 @@ struct XMLKeyedEncodingContainer: KeyedEncodingContainerProtocol { } } - let elementEncoder: (T, Key, Box) throws -> () = { value, key, box in + let elementEncoder: (T, Key, Box) throws -> () = { _, key, box in mySelf.container.withShared { container in container.elements[mySelf._converted(key).stringValue] = box } diff --git a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift index 845a0d4a..77ebf513 100644 --- a/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift +++ b/Tests/XMLCoderTests/DynamicNodeEncodingTest.swift @@ -80,16 +80,16 @@ final class DynamicNodeEncodingTest: XCTestCase { let book1 = Book(id: 123, title: "Cat in the Hat", categories: [ - Category(main: true, value: "Kids"), - Category(main: false, value: "Wildlife") - ]) + 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") - ]) + Category(main: true, value: "Classics"), + Category(main: false, value: "News"), + ]) let library = Library(count: 2, books: [book1, book2]) let encoder = XMLEncoder() @@ -109,7 +109,6 @@ final class DynamicNodeEncodingTest: XCTestCase { func testDecode() { do { - let decoder = XMLDecoder() decoder.errorContextLength = 10 @@ -194,5 +193,5 @@ final class DynamicNodeEncodingTest: XCTestCase { ("testEncode", testEncode), ("testDecode", testDecode), ("testEncodeDecode", testEncodeDecode), - ] + ] } From f5a8578c43f51434d37940a1ea08705299f54c05 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 14:27:35 -0500 Subject: [PATCH 14/24] Add test coverage for String+Extensions --- .../Auxiliary/String+ExtensionsTests.swift | 43 +++++++++++++++++++ XMLCoder.xcodeproj/project.pbxproj | 4 ++ 2 files changed, 47 insertions(+) create mode 100644 Tests/XMLCoderTests/Auxiliary/String+ExtensionsTests.swift diff --git a/Tests/XMLCoderTests/Auxiliary/String+ExtensionsTests.swift b/Tests/XMLCoderTests/Auxiliary/String+ExtensionsTests.swift new file mode 100644 index 00000000..369778c7 --- /dev/null +++ b/Tests/XMLCoderTests/Auxiliary/String+ExtensionsTests.swift @@ -0,0 +1,43 @@ +// +// String+ExtensionsTests.swift +// XMLCoderTests +// +// Created by Joseph Mattiello on 1/31/19. +// + +import XCTest +@testable import XMLCoder + +class StringExtensionsTests: XCTestCase { + func testCapitalizingFirstLetter() { + let testStrings = ["lower", "UPPER", "snake_cased", "RaNdOm", ""] + let expected = ["Lower", "UPPER", "Snake_cased", "RaNdOm", ""] + let converted = testStrings.map { $0.capitalizingFirstLetter() } + + XCTAssertEqual(expected, converted) + + // Mutable version + let mutated: [String] = testStrings.map { s in + var s = s + s.capitalizeFirstLetter() + return s + } + XCTAssertEqual(expected, mutated) + } + + func testLowercasingFirstLetter() { + let testStrings = ["lower", "UPPER", "snake_cased", "RaNdOm", ""] + let expected = ["lower", "uPPER", "snake_cased", "raNdOm", ""] + let converted = testStrings.map { $0.lowercasingFirstLetter() } + + XCTAssertEqual(expected, converted) + + // Mutable version + let mutated: [String] = testStrings.map { s in + var s = s + s.lowercaseFirstLetter() + return s + } + XCTAssertEqual(expected, mutated) + } +} diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 42ceec1f..48057411 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ A61DCCD821DF9CA200C0A19D /* ClassTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61DCCD621DF8DB300C0A19D /* ClassTests.swift */; }; A61FE03921E4D60B0015D993 /* UnkeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */; }; A61FE03C21E4EAB10015D993 /* KeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */; }; + B34B3C08220381AC00BCBA30 /* String+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34B3C07220381AB00BCBA30 /* String+ExtensionsTests.swift */; }; B35157CE21F986DD009CA0CC /* DynamicNodeEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */; }; B3BE1D612202C1F600259831 /* DynamicNodeEncodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */; }; B3BE1D632202CB1400259831 /* XMLEncoderImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */; }; @@ -132,6 +133,7 @@ A61DCCD621DF8DB300C0A19D /* ClassTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassTests.swift; sourceTree = ""; }; A61FE03721E4D4F10015D993 /* UnkeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnkeyedIntTests.swift; sourceTree = ""; }; A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = ""; }; + B34B3C07220381AB00BCBA30 /* String+ExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+ExtensionsTests.swift"; sourceTree = ""; }; B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncoding.swift; sourceTree = ""; }; B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncodingTest.swift; sourceTree = ""; }; B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLEncoderImplementation.swift; sourceTree = ""; }; @@ -249,6 +251,7 @@ BF63EEFF21CCDED2001D38C5 /* XMLStackParserTests.swift */, BF63EF6821D0FDB5001D38C5 /* XMLHeaderTests.swift */, BF63EF6A21D10284001D38C5 /* XMLElementTests.swift */, + B34B3C07220381AB00BCBA30 /* String+ExtensionsTests.swift */, ); path = Auxiliary; sourceTree = ""; @@ -634,6 +637,7 @@ OBJ_85 /* NodeEncodingStrategyTests.swift in Sources */, OBJ_86 /* NoteTest.swift in Sources */, BF63EF0C21CD7F28001D38C5 /* EmptyTests.swift in Sources */, + B34B3C08220381AC00BCBA30 /* String+ExtensionsTests.swift in Sources */, B3BE1D612202C1F600259831 /* DynamicNodeEncodingTest.swift in Sources */, BF9457F721CBB6BC005ACFDE /* DataTests.swift in Sources */, BF9457EE21CBB6BC005ACFDE /* IntTests.swift in Sources */, From a110b83548c7b0f300a3ca2be7be9e4799bbc29a Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Wed, 6 Feb 2019 00:59:57 -0500 Subject: [PATCH 15/24] Fix lowercasingFirstLetter was capitalized --- Sources/XMLCoder/Auxiliaries/String+Extensions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/String+Extensions.swift b/Sources/XMLCoder/Auxiliaries/String+Extensions.swift index 1dc45c6c..14a1b920 100644 --- a/Sources/XMLCoder/Auxiliaries/String+Extensions.swift +++ b/Sources/XMLCoder/Auxiliaries/String+Extensions.swift @@ -33,13 +33,13 @@ extension StringProtocol { func lowercasingFirstLetter() -> Self { // avoid lowercasing single letters (I), or capitalized multiples (AThing ! to aThing, leave as AThing) - guard count > 1, !(String(prefix(2)) == prefix(2).uppercased()) else { + guard count > 1, !(String(prefix(2)) == prefix(2).lowercased()) else { return self } return Self(prefix(1).lowercased() + dropFirst())! } mutating func lowercaseFirstLetter() { - self = capitalizingFirstLetter() + self = lowercasingFirstLetter() } } From 559f35a5525fd3fc46f9803cffcb9b0ab859562b Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:58:24 -0500 Subject: [PATCH 16/24] closes #12, support attributed intrinsic values Add intrinsic encoding decoding with attributes support --- .../XMLCoder/Auxiliaries/Box/KeyedBox.swift | 7 ++ .../Auxiliaries/XMLCoderElement.swift | 7 +- .../Decoder/XMLDecoderImplementation.swift | 1 - .../Decoder/XMLKeyedDecodingContainer.swift | 23 ++-- .../AttributedIntrinsicTest.swift | 102 ++++++++++++++++++ XMLCoder.xcodeproj/project.pbxproj | 4 + 6 files changed, 134 insertions(+), 10 deletions(-) create mode 100644 Tests/XMLCoderTests/AttributedIntrinsicTest.swift diff --git a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift index 9d572993..ab92c55b 100644 --- a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift +++ b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift @@ -104,6 +104,13 @@ extension KeyedBox: Box { } } +extension KeyedBox { + var value: SimpleBox? { + guard elements.count == 1, let value = elements["value"] as? SimpleBox ?? elements[""] as? SimpleBox, !value.isNull else { return nil } + return value + } +} + extension KeyedBox: CustomStringConvertible { var description: String { return "{attributes: \(attributes), elements: \(elements)}" diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index fc1f5cd6..409b3505 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -47,7 +47,7 @@ struct XMLCoderElement: Equatable { func flatten() -> KeyedBox { let attributes = self.attributes.mapValues { StringBox($0) } - let keyedElements: [String: Box] = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in + var keyedElements: [String: Box] = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in var result = result let key = element.key @@ -93,6 +93,11 @@ struct XMLCoderElement: Equatable { return result } + // Handle attributed unkeyed valye zap + // Value should be zap. Detect only when no other elements exist + if keyedElements.isEmpty, let value = value { + keyedElements["value"] = StringBox(value) + } let keyedBox = KeyedBox(elements: keyedElements, attributes: attributes) return keyedBox diff --git a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift index 37e48534..699c4067 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoderImplementation.swift @@ -308,7 +308,6 @@ extension XMLDecoderImplementation { if type == Date.self || type == NSDate.self { let date: Date = try unbox(box) - decoded = date as? T } else if type == Data.self || type == NSData.self { let data: Data = try unbox(box) diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index b1f4d55d..5e7904b9 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -126,15 +126,18 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { public func decode( _ type: T.Type, forKey key: Key ) throws -> T { - let attributeNotFound = container.withShared { keyedBox in - keyedBox.attributes[key.stringValue] == nil + let attributeFound = container.withShared { keyedBox in + keyedBox.attributes[key.stringValue] != nil } - let elementNotFound = container.withShared { keyedBox in - keyedBox.elements[key.stringValue] == nil + + let elementFound = container.withShared { keyedBox -> Bool in + keyedBox.elements[key.stringValue] != nil || keyedBox.value != nil } - if let type = type as? AnyEmptySequence.Type, attributeNotFound, - elementNotFound, let result = type.init() as? T { + if let type = type as? AnyEmptySequence.Type, + !attributeFound, + !elementFound, + let result = type.init() as? T { return result } @@ -163,8 +166,12 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { _ type: T.Type, forKey key: Key ) throws -> T { - let elementOrNil = container.withShared { keyedBox in - keyedBox.elements[key.stringValue] + let elementOrNil = container.withShared { keyedBox -> KeyedBox.Element? in + if ["value", ""].contains(key.stringValue) { + return keyedBox.value + } else { + return keyedBox.elements[key.stringValue] + } } let attributeOrNil = container.withShared { keyedBox in diff --git a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift new file mode 100644 index 00000000..51b2990e --- /dev/null +++ b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift @@ -0,0 +1,102 @@ +// +// AttributedIntrinsicTest.swift +// XMLCoderTests +// +// Created by Joseph Mattiello on 1/23/19. +// + +import Foundation +import XCTest +@testable import XMLCoder + +let fooXML = """ + +456 +""".data(using: .utf8)! + +private struct Foo: Codable, DynamicNodeEncoding { + let id: String + let value: String + + enum CodingKeys: String, CodingKey { + case id + case value + } + + static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.id: + return .attribute + default: + return .element + } + } +} + +private struct FooEmptyKeyed: Codable, DynamicNodeEncoding { + let id: String + let unkeyedValue: Int + + enum CodingKeys: String, CodingKey { + case id + case unkeyedValue = "" + } + + static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case CodingKeys.id: + return .attribute + default: + return .element + } + } +} + +final class AttributedIntrinsicTest: XCTestCase { + + func testEncode() { + let encoder = XMLEncoder() + encoder.outputFormatting = [] + + let foo1 = FooEmptyKeyed(id: "123", unkeyedValue: 456) + + let header = XMLHeader(version: 1.0, encoding: "UTF-8") + do { + let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header) + let xmlString = String(data: encoded, encoding: .utf8) + XCTAssertNotNil(xmlString) + print(xmlString!) + + // Test string equivlancy + let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) + let originalXML = String(data: fooXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(encodedXML, originalXML) + } catch { + print("Test threw error: " + error.localizedDescription) + XCTFail(error.localizedDescription) + } + } + + func testDecode() { + do { + let decoder = XMLDecoder() + decoder.errorContextLength = 10 + + let foo1 = try decoder.decode(Foo.self, from: fooXML) + XCTAssertEqual(foo1.id, "123") + XCTAssertEqual(foo1.value, "456") + + let foo2 = try decoder.decode(FooEmptyKeyed.self, from: fooXML) + XCTAssertEqual(foo2.id, "123") + XCTAssertEqual(foo2.unkeyedValue, 456) + } catch { + print("Test threw error: " + error.localizedDescription) + XCTFail(error.localizedDescription) + } + } + + static var allTests = [ + ("testEncode", testEncode), + ("testDecode", testDecode), + ] +} diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 48057411..d1fa530b 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ A61FE03C21E4EAB10015D993 /* KeyedIntTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */; }; B34B3C08220381AC00BCBA30 /* String+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B34B3C07220381AB00BCBA30 /* String+ExtensionsTests.swift */; }; B35157CE21F986DD009CA0CC /* DynamicNodeEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */; }; + B3B6902E220A71DF0084D407 /* AttributedIntrinsicTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */; }; B3BE1D612202C1F600259831 /* DynamicNodeEncodingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */; }; B3BE1D632202CB1400259831 /* XMLEncoderImplementation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */; }; B3BE1D652202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE1D642202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift */; }; @@ -135,6 +136,7 @@ A61FE03A21E4EA8B0015D993 /* KeyedIntTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyedIntTests.swift; sourceTree = ""; }; B34B3C07220381AB00BCBA30 /* String+ExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+ExtensionsTests.swift"; sourceTree = ""; }; B35157CD21F986DD009CA0CC /* DynamicNodeEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncoding.swift; sourceTree = ""; }; + B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedIntrinsicTest.swift; sourceTree = ""; }; B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DynamicNodeEncodingTest.swift; sourceTree = ""; }; B3BE1D622202CB1400259831 /* XMLEncoderImplementation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLEncoderImplementation.swift; sourceTree = ""; }; B3BE1D642202CB7200259831 /* XMLEncoderImplementation+SingleValueEncodingContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "XMLEncoderImplementation+SingleValueEncodingContainer.swift"; sourceTree = ""; }; @@ -378,6 +380,7 @@ OBJ_38 /* RelationshipsTest.swift */, BF63EF1D21CEC99A001D38C5 /* BenchmarkTests.swift */, D1FC040421C7EF8200065B43 /* RJISample.swift */, + B3B6902D220A71DF0084D407 /* AttributedIntrinsicTest.swift */, B3BE1D602202C1F600259831 /* DynamicNodeEncodingTest.swift */, A61DCCD621DF8DB300C0A19D /* ClassTests.swift */, D14D8A8521F1D6B300B0D31A /* SingleChildTests.swift */, @@ -618,6 +621,7 @@ A61FE03921E4D60B0015D993 /* UnkeyedIntTests.swift in Sources */, BF63EF6B21D10284001D38C5 /* XMLElementTests.swift in Sources */, BF9457ED21CBB6BC005ACFDE /* BoolTests.swift in Sources */, + B3B6902E220A71DF0084D407 /* AttributedIntrinsicTest.swift in Sources */, D1FC040521C7EF8200065B43 /* RJISample.swift in Sources */, BF63EF0A21CD7C1A001D38C5 /* URLTests.swift in Sources */, BF9457CE21CBB516005ACFDE /* StringBoxTests.swift in Sources */, From 04290c6133c78165cab25990b779471d8e267583 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sat, 9 Feb 2019 15:03:43 +0000 Subject: [PATCH 17/24] Fix formatting --- Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift | 2 +- Tests/XMLCoderTests/AttributedIntrinsicTest.swift | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift index 5a01f610..65c0419a 100644 --- a/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLUnkeyedDecodingContainer.swift @@ -94,7 +94,7 @@ struct XMLUnkeyedDecodingContainer: UnkeyedDecodingContainer { if let type = type as? AnyArray.Type, let keyedBox = container .withShared({ $0[self.currentIndex] as? KeyedBox }), - keyedBox.attributes.count == 0, + keyedBox.attributes.isEmpty, keyedBox.elements.count == 1, let firstKey = keyedBox.elements.keys.first, let unkeyedBox = keyedBox.elements[firstKey] { diff --git a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift index 51b2990e..2172f9d4 100644 --- a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift +++ b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift @@ -53,7 +53,6 @@ private struct FooEmptyKeyed: Codable, DynamicNodeEncoding { } final class AttributedIntrinsicTest: XCTestCase { - func testEncode() { let encoder = XMLEncoder() encoder.outputFormatting = [] @@ -98,5 +97,5 @@ final class AttributedIntrinsicTest: XCTestCase { static var allTests = [ ("testEncode", testEncode), ("testDecode", testDecode), - ] + ] } From b1b096c5247997b0be773055f6058d3db1aed154 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Sat, 9 Feb 2019 15:39:02 +0000 Subject: [PATCH 18/24] Fix compilation error --- Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index b9208011..409b3505 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -47,7 +47,7 @@ struct XMLCoderElement: Equatable { func flatten() -> KeyedBox { let attributes = self.attributes.mapValues { StringBox($0) } - let keyedElements: [String: Box] = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in + var keyedElements: [String: Box] = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in var result = result let key = element.key From c5af41b12bd67ead11d8b2fca21276c7bbdf5581 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 21 Feb 2019 16:04:19 -0500 Subject: [PATCH 19/24] #12 - Fix broken unit tests --- Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index 5e7904b9..dac36e08 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -168,7 +168,7 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { ) throws -> T { let elementOrNil = container.withShared { keyedBox -> KeyedBox.Element? in if ["value", ""].contains(key.stringValue) { - return keyedBox.value + return keyedBox.elements[key.stringValue] ?? keyedBox.value } else { return keyedBox.elements[key.stringValue] } From e89383e7fece220d6b3e8e3f1516612667f74aaa Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 21 Feb 2019 23:45:56 -0500 Subject: [PATCH 20/24] #12 - Add enum associated value intrinsic encoding --- .../AttributedIntrinsicTest.swift | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift index 2172f9d4..00dc2bad 100644 --- a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift +++ b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift @@ -99,3 +99,131 @@ final class AttributedIntrinsicTest: XCTestCase { ("testDecode", testDecode), ] } + +// MARK: - Enums + +let attributedEnumXML = """ + +ABC123 +""".data(using: .utf8)! + +private struct Foo2: Codable { + let number: [FooNumber] +} + +private struct FooNumber: Codable, DynamicNodeEncoding { + public let type: FooEnum + + public init(type: FooEnum) { + self.type = type + } + + enum CodingKeys: String, CodingKey { + case type + case typeValue = "" + } + + public static func nodeEncoding(forKey key: CodingKey) -> XMLEncoder.NodeEncoding { + switch key { + case FooNumber.CodingKeys.type: return .attribute + default: return .element + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + type = try container.decode(FooEnum.self, forKey: .type) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch type { + case let .string(value): + try container.encode("string", forKey: .type) + try container.encode(value, forKey: .typeValue) + case let .int(value): + try container.encode("int", forKey: .type) + try container.encode(value, forKey: .typeValue) + } + } +} + +private enum FooEnum: Equatable, Codable { + private enum CodingKeys: String, CodingKey { + case string + case int + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + if let value = try values.decodeIfPresent(String.self, forKey: .string) { + self = .string(value) + return + } else if let value = try values.decodeIfPresent(Int.self, forKey: .int) { + self = .int(value) + return + } else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, + debugDescription: "No coded value for string or int")) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .string(value): + try container.encode(value, forKey: .string) + case let .int(value): + try container.encode(value, forKey: .int) + } + } + + case string(String) + case int(Int) +} + +final class AttributedEnumIntrinsicTest: XCTestCase { + func testEncode() { + let encoder = XMLEncoder() + encoder.outputFormatting = [] + + let foo1 = Foo2(number: [FooNumber(type: FooEnum.string("ABC")), FooNumber(type: FooEnum.int(123))]) + + let header = XMLHeader(version: 1.0, encoding: "UTF-8") + do { + let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header) + let xmlString = String(data: encoded, encoding: .utf8) + XCTAssertNotNil(xmlString) + print(xmlString!) + + // Test string equivlancy + let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) + let originalXML = String(data: attributedEnumXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(encodedXML, originalXML) + } catch { + print("Test threw error: " + error.localizedDescription) + XCTFail(error.localizedDescription) + } + } + + // TODO: Fix decoding +// func testDecode() { +// do { +// let decoder = XMLDecoder() +// decoder.errorContextLength = 10 +// +// let foo = try decoder.decode(Foo2.self, from: attributedEnumXML) +// XCTAssertEqual(foo.number[0].type, FooEnum.string("ABC")) +// XCTAssertEqual(foo.number[1].type, FooEnum.int(123)) +// } catch { +// print("Test threw error: " + error.localizedDescription) +// XCTFail(error.localizedDescription) +// } +// } + + static var allTests = [ + ("testEncode", testEncode), +// ("testDecode", testDecode), + ] +} From aaa9e1a190e3188d5669f3d7905017339e58058e Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 22 Feb 2019 09:12:12 +0000 Subject: [PATCH 21/24] Fix comment typos --- Tests/XMLCoderTests/AttributedIntrinsicTest.swift | 4 ++-- Tests/XMLCoderTests/BooksTest.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift index 00dc2bad..6576d5d4 100644 --- a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift +++ b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift @@ -66,7 +66,7 @@ final class AttributedIntrinsicTest: XCTestCase { XCTAssertNotNil(xmlString) print(xmlString!) - // Test string equivlancy + // Test string equivalency let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) let originalXML = String(data: fooXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) XCTAssertEqual(encodedXML, originalXML) @@ -197,7 +197,7 @@ final class AttributedEnumIntrinsicTest: XCTestCase { XCTAssertNotNil(xmlString) print(xmlString!) - // Test string equivlancy + // Test string equivalency let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) let originalXML = String(data: attributedEnumXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) XCTAssertEqual(encodedXML, originalXML) diff --git a/Tests/XMLCoderTests/BooksTest.swift b/Tests/XMLCoderTests/BooksTest.swift index de1b5785..f596a086 100644 --- a/Tests/XMLCoderTests/BooksTest.swift +++ b/Tests/XMLCoderTests/BooksTest.swift @@ -218,7 +218,7 @@ final class BooksTest: XCTestCase { XCTAssertEqual(book1, book2) - // Test string equivlancy + // Test string equivalency let encodedXML = String(data: data, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) let originalXML = String(data: bookXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) XCTAssertEqual(encodedXML, originalXML) From a0de61b226b13956d434e3ec76ae4f160324165b Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 22 Feb 2019 09:16:31 +0000 Subject: [PATCH 22/24] Fix line length coding style --- Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift | 6 +++++- Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift index 8eb15ea1..9d4d5156 100644 --- a/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift +++ b/Sources/XMLCoder/Auxiliaries/Box/KeyedBox.swift @@ -108,7 +108,11 @@ extension KeyedBox: Box { extension KeyedBox { var value: SimpleBox? { - guard elements.count == 1, let value = elements["value"] as? SimpleBox ?? elements[""] as? SimpleBox, !value.isNull else { return nil } + guard + elements.count == 1, + let value = elements["value"] as? SimpleBox + ?? elements[""] as? SimpleBox, + !value.isNull else { return nil } return value } } diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 409b3505..83a58fbf 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -47,7 +47,7 @@ struct XMLCoderElement: Equatable { func flatten() -> KeyedBox { let attributes = self.attributes.mapValues { StringBox($0) } - var keyedElements: [String: Box] = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in + var keyedElements = elements.reduce([String: Box]()) { (result, element) -> [String: Box] in var result = result let key = element.key From ff3f19eea1d0c93bcc137936ac41c884f9f324ec Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 22 Feb 2019 09:18:46 +0000 Subject: [PATCH 23/24] Fix comment typo, rely on more type inference --- Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift | 2 +- Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift index 83a58fbf..db0045c1 100644 --- a/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift +++ b/Sources/XMLCoder/Auxiliaries/XMLCoderElement.swift @@ -93,7 +93,7 @@ struct XMLCoderElement: Equatable { return result } - // Handle attributed unkeyed valye zap + // Handle attributed unkeyed value zap // Value should be zap. Detect only when no other elements exist if keyedElements.isEmpty, let value = value { keyedElements["value"] = StringBox(value) diff --git a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift index dac36e08..5f5cf63d 100644 --- a/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift +++ b/Sources/XMLCoder/Decoder/XMLKeyedDecodingContainer.swift @@ -130,7 +130,7 @@ struct XMLKeyedDecodingContainer: KeyedDecodingContainerProtocol { keyedBox.attributes[key.stringValue] != nil } - let elementFound = container.withShared { keyedBox -> Bool in + let elementFound = container.withShared { keyedBox in keyedBox.elements[key.stringValue] != nil || keyedBox.value != nil } From 7a4f94f0da8cf4106973f11b82c0f408337808af Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 22 Feb 2019 09:20:58 +0000 Subject: [PATCH 24/24] Make tests throwing, remove print usage --- .../AttributedIntrinsicTest.swift | 91 +++++++------------ 1 file changed, 34 insertions(+), 57 deletions(-) diff --git a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift index 6576d5d4..b64ce0fd 100644 --- a/Tests/XMLCoderTests/AttributedIntrinsicTest.swift +++ b/Tests/XMLCoderTests/AttributedIntrinsicTest.swift @@ -53,45 +53,34 @@ private struct FooEmptyKeyed: Codable, DynamicNodeEncoding { } final class AttributedIntrinsicTest: XCTestCase { - func testEncode() { + func testEncode() throws { let encoder = XMLEncoder() encoder.outputFormatting = [] let foo1 = FooEmptyKeyed(id: "123", unkeyedValue: 456) let header = XMLHeader(version: 1.0, encoding: "UTF-8") - do { - let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header) - let xmlString = String(data: encoded, encoding: .utf8) - XCTAssertNotNil(xmlString) - print(xmlString!) - - // Test string equivalency - let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) - let originalXML = String(data: fooXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) - XCTAssertEqual(encodedXML, originalXML) - } catch { - print("Test threw error: " + error.localizedDescription) - XCTFail(error.localizedDescription) - } + let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header) + let xmlString = String(data: encoded, encoding: .utf8) + XCTAssertNotNil(xmlString) + + // Test string equivalency + let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) + let originalXML = String(data: fooXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(encodedXML, originalXML) } - func testDecode() { - do { - let decoder = XMLDecoder() - decoder.errorContextLength = 10 - - let foo1 = try decoder.decode(Foo.self, from: fooXML) - XCTAssertEqual(foo1.id, "123") - XCTAssertEqual(foo1.value, "456") - - let foo2 = try decoder.decode(FooEmptyKeyed.self, from: fooXML) - XCTAssertEqual(foo2.id, "123") - XCTAssertEqual(foo2.unkeyedValue, 456) - } catch { - print("Test threw error: " + error.localizedDescription) - XCTFail(error.localizedDescription) - } + func testDecode() throws { + let decoder = XMLDecoder() + decoder.errorContextLength = 10 + + let foo1 = try decoder.decode(Foo.self, from: fooXML) + XCTAssertEqual(foo1.id, "123") + XCTAssertEqual(foo1.value, "456") + + let foo2 = try decoder.decode(FooEmptyKeyed.self, from: fooXML) + XCTAssertEqual(foo2.id, "123") + XCTAssertEqual(foo2.unkeyedValue, 456) } static var allTests = [ @@ -184,42 +173,30 @@ private enum FooEnum: Equatable, Codable { } final class AttributedEnumIntrinsicTest: XCTestCase { - func testEncode() { + func testEncode() throws { let encoder = XMLEncoder() encoder.outputFormatting = [] let foo1 = Foo2(number: [FooNumber(type: FooEnum.string("ABC")), FooNumber(type: FooEnum.int(123))]) let header = XMLHeader(version: 1.0, encoding: "UTF-8") - do { - let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header) - let xmlString = String(data: encoded, encoding: .utf8) - XCTAssertNotNil(xmlString) - print(xmlString!) - - // Test string equivalency - let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) - let originalXML = String(data: attributedEnumXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) - XCTAssertEqual(encodedXML, originalXML) - } catch { - print("Test threw error: " + error.localizedDescription) - XCTFail(error.localizedDescription) - } + let encoded = try encoder.encode(foo1, withRootKey: "foo", header: header) + let xmlString = String(data: encoded, encoding: .utf8) + XCTAssertNotNil(xmlString) + // Test string equivalency + let encodedXML = xmlString!.trimmingCharacters(in: .whitespacesAndNewlines) + let originalXML = String(data: attributedEnumXML, encoding: .utf8)!.trimmingCharacters(in: .whitespacesAndNewlines) + XCTAssertEqual(encodedXML, originalXML) } // TODO: Fix decoding -// func testDecode() { -// do { -// let decoder = XMLDecoder() -// decoder.errorContextLength = 10 +// func testDecode() throws { +// let decoder = XMLDecoder() +// decoder.errorContextLength = 10 // -// let foo = try decoder.decode(Foo2.self, from: attributedEnumXML) -// XCTAssertEqual(foo.number[0].type, FooEnum.string("ABC")) -// XCTAssertEqual(foo.number[1].type, FooEnum.int(123)) -// } catch { -// print("Test threw error: " + error.localizedDescription) -// XCTFail(error.localizedDescription) -// } +// let foo = try decoder.decode(Foo2.self, from: attributedEnumXML) +// XCTAssertEqual(foo.number[0].type, FooEnum.string("ABC")) +// XCTAssertEqual(foo.number[1].type, FooEnum.int(123)) // } static var allTests = [