From 05e3c2dd60048a6f5a86408fd7a266a1f32b3e87 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:19:20 -0500 Subject: [PATCH 01/16] 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 5dbe1d7482f30c928ef0e6eb6d4ea3f8506a0df9 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:31:24 -0500 Subject: [PATCH 02/16] 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 9ae94dc91b5326ee51049421867f22dc01abc6c3 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:38:09 -0500 Subject: [PATCH 03/16] 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 601e33bdeb04f6b0d84497550cac1afa10f14294 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:40:09 -0500 Subject: [PATCH 04/16] 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 551d4843c5eb58a83f8fbfe274961bcb0249a0d4 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:50:46 -0500 Subject: [PATCH 05/16] 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 0d8c2bc4..7bcc20d1 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -648,6 +648,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 bce416aa2d295f2f5e0d7ac53cc1aa11d0675162 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:57:53 -0500 Subject: [PATCH 06/16] 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 bd24a08b17d8305a0b4a617adc9f93c3a9ad37e9 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:28:22 -0500 Subject: [PATCH 07/16] 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 07431563f586f960123c6777e19e41ef0676f7a1 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:21:19 -0500 Subject: [PATCH 08/16] 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 7bcc20d1..bdb08e91 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -296,7 +296,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 @@ -316,7 +316,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 5eedcea50d4a952177631bd7f92f6515dba1525a Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 01:45:14 -0500 Subject: [PATCH 09/16] 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 bdb08e91..91a88441 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -291,378 +291,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 323658513950db7a36579999cda3788ce0717c68 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 03:50:37 -0500 Subject: [PATCH 10/16] 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 0d77ee6c20dfab788bb9cec9c5d4c84d72ff5d01 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 05:00:01 -0500 Subject: [PATCH 11/16] 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 295d7323ba79f7079a0ba32c016c873dbe7633e6 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 05:01:53 -0500 Subject: [PATCH 12/16] 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 b9ee4fae9a36ee19f0c92f8549bf2fea3d6896c6 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 14:01:12 -0500 Subject: [PATCH 13/16] 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 2881481c6eeea70152cbb4d15b14aad3ba146f79 Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 14:27:35 -0500 Subject: [PATCH 14/16] 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 2a3ae40946b8d33a2d4e03c377800429e8d287cc Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Wed, 6 Feb 2019 00:59:57 -0500 Subject: [PATCH 15/16] 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 ef4f450b5929c38eb5f77d9e84cad486dddb124c Mon Sep 17 00:00:00 2001 From: Joe Mattiello Date: Thu, 31 Jan 2019 00:58:24 -0500 Subject: [PATCH 16/16] 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 */,