From d21bb2e64b5c8bf5bd6a5bfbe096b14aaefc554b Mon Sep 17 00:00:00 2001 From: Vincent Esche Date: Fri, 21 Dec 2018 21:01:58 +0100 Subject: [PATCH] Extracted URL coding logic into `URLBox` with corresponding unit tests --- Sources/XMLCoder/Box/URLBox.swift | 53 ++++++++++++++ Sources/XMLCoder/Decoder/XMLDecoder.swift | 29 ++++---- Sources/XMLCoder/Encoder/XMLEncoder.swift | 29 +++----- Tests/XMLCoderTests/Box/URLBoxTests.swift | 61 ++++++++++++++++ Tests/XMLCoderTests/Minimal/URLTests.swift | 81 ++++++++++++++++++++++ XMLCoder.xcodeproj/project.pbxproj | 12 ++++ 6 files changed, 234 insertions(+), 31 deletions(-) create mode 100644 Sources/XMLCoder/Box/URLBox.swift create mode 100644 Tests/XMLCoderTests/Box/URLBoxTests.swift create mode 100644 Tests/XMLCoderTests/Minimal/URLTests.swift diff --git a/Sources/XMLCoder/Box/URLBox.swift b/Sources/XMLCoder/Box/URLBox.swift new file mode 100644 index 00000000..6d929307 --- /dev/null +++ b/Sources/XMLCoder/Box/URLBox.swift @@ -0,0 +1,53 @@ +// +// URLBox.swift +// XMLCoder +// +// Created by Vincent Esche on 12/21/18. +// + +import Foundation + +struct URLBox: Equatable { + typealias Unboxed = URL + + let unboxed: Unboxed + + init(_ unboxed: Unboxed) { + self.unboxed = unboxed + } + + init?(xmlString: String) { + guard let unboxed = Unboxed(string: xmlString) else { + return nil + } + self.init(unboxed) + } + + func unbox() -> Unboxed { + return self.unboxed + } +} + +extension URLBox: Box { + var isNull: Bool { + return false + } + + var isFragment: Bool { + return true + } + + func xmlString() -> String? { + return self.unboxed.absoluteString + } +} + +extension URLBox: SimpleBox { + +} + +extension URLBox: CustomStringConvertible { + var description: String { + return self.unboxed.description + } +} diff --git a/Sources/XMLCoder/Decoder/XMLDecoder.swift b/Sources/XMLCoder/Decoder/XMLDecoder.swift index 6049a937..df7ed380 100644 --- a/Sources/XMLCoder/Decoder/XMLDecoder.swift +++ b/Sources/XMLCoder/Decoder/XMLDecoder.swift @@ -575,6 +575,21 @@ extension _XMLDecoder { return try closure(self) } } + + internal func unbox(_ box: Box) throws -> URL? { + guard !box.isNull else { return nil } + + guard let string = (box as? StringBox)?.unbox() else { return nil } + + guard let urlBox = URLBox(xmlString: string) else { + throw DecodingError.dataCorrupted(DecodingError.Context( + codingPath: codingPath, + debugDescription: "Encountered Data is not valid Base64" + )) + } + + return urlBox.unbox() + } func unbox(_ box: Box) throws -> T? { let decoded: T @@ -586,18 +601,8 @@ extension _XMLDecoder { guard let data: Data = try unbox(box) else { return nil } decoded = data as! T } else if type == URL.self || type == NSURL.self { - guard let urlString: String = try unbox(box) else { - return nil - } - - guard let url = URL(string: urlString) else { - throw DecodingError.dataCorrupted(DecodingError.Context( - codingPath: codingPath, - debugDescription: "Invalid URL string." - )) - } - - decoded = (url as! T) + guard let data: URL = try unbox(box) else { return nil } + decoded = data as! T } else if type == Decimal.self || type == NSDecimalNumber.self { guard let decimal: Decimal = try unbox(box) else { return nil } decoded = decimal as! T diff --git a/Sources/XMLCoder/Encoder/XMLEncoder.swift b/Sources/XMLCoder/Encoder/XMLEncoder.swift index b0064066..ef851ce9 100644 --- a/Sources/XMLCoder/Encoder/XMLEncoder.swift +++ b/Sources/XMLCoder/Encoder/XMLEncoder.swift @@ -265,12 +265,7 @@ open class XMLEncoder { ) encoder.nodeEncodings.append(options.nodeEncodingStrategy.nodeEncodings(forType: T.self, with: encoder)) - guard let topLevel = try encoder.boxOrNil(value) else { - throw EncodingError.invalidValue(value, EncodingError.Context( - codingPath: [], - debugDescription: "Top-level \(T.self) did not encode any values." - )) - } + let topLevel = try encoder.box(value) let elementOrNone: _XMLElement? @@ -552,22 +547,18 @@ extension _XMLEncoder { } } - func box(_ value: T) throws -> Box { - return try self.boxOrNil(value) ?? KeyedBox() + func box(_ value: URL) -> SimpleBox { + return URLBox(value) } - - // This method is called "box_" instead of "box" to disambiguate it from the overloads. Because the return type here is different from all of the "box" overloads (and is more general), any "box" calls in here would call back into "box" recursively instead of calling the appropriate overload, which is not what we want. - internal func boxOrNil(_ value: T) throws -> Box? { + + internal func box(_ value: T) throws -> Box { if T.self == Date.self || T.self == NSDate.self { return try box(value as! Date) - } - if T.self == Data.self || T.self == NSData.self { + } else if T.self == Data.self || T.self == NSData.self { return try box(value as! Data) - } - if T.self == URL.self || T.self == NSURL.self { - return box((value as! URL).absoluteString) - } - if T.self == Decimal.self || T.self == NSDecimalNumber.self { + } else if T.self == URL.self || T.self == NSURL.self { + return box(value as! URL) + } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { return box(value as! Decimal) } @@ -576,7 +567,7 @@ extension _XMLEncoder { // The top container should be a new container. guard storage.count > depth else { - return nil + return KeyedBox() } return storage.popContainer() diff --git a/Tests/XMLCoderTests/Box/URLBoxTests.swift b/Tests/XMLCoderTests/Box/URLBoxTests.swift new file mode 100644 index 00000000..27036e0d --- /dev/null +++ b/Tests/XMLCoderTests/Box/URLBoxTests.swift @@ -0,0 +1,61 @@ +// +// URLBoxTests.swift +// XMLCoderTests +// +// Created by Vincent Esche on 12/21/18. +// + +import XCTest +@testable import XMLCoder + +class URLBoxTests: XCTestCase { + typealias Boxed = URLBox + + func testUnbox() { + let values: [Boxed.Unboxed] = [ + URL(string: "file:///")!, + URL(string: "http://example.com")!, + ] + + for unboxed in values { + let box = Boxed(unboxed) + XCTAssertEqual(box.unbox(), unboxed) + } + } + + func testXMLString() { + let values: [(Boxed.Unboxed, String)] = [ + (URL(string: "file:///")!, "file:///"), + (URL(string: "http://example.com")!, "http://example.com"), + ] + + for (bool, string) in values { + let box = Boxed(bool) + XCTAssertEqual(box.xmlString(), string) + } + } + + func testValidValues() { + let values: [String] = [ + "file:///", + "http://example.com", + ] + + for string in values { + let box = Boxed(xmlString: string) + XCTAssertNotNil(box) + } + } + + func testInvalidValues() { + let values: [String] = [ + "foo\nbar", + "", + ] + + for string in values { + let box = Boxed(xmlString: string) + XCTAssertNil(box) + } + } +} diff --git a/Tests/XMLCoderTests/Minimal/URLTests.swift b/Tests/XMLCoderTests/Minimal/URLTests.swift new file mode 100644 index 00000000..62fafe51 --- /dev/null +++ b/Tests/XMLCoderTests/Minimal/URLTests.swift @@ -0,0 +1,81 @@ +// +// URLTests.swift +// XMLCoderTests +// +// Created by Vincent Esche on 12/19/18. +// + +import XCTest +@testable import XMLCoder + +class URLTests: XCTestCase { + typealias Value = URL + + struct Container: Codable, Equatable { + let value: Value + } + + let values: [(Value, String)] = [ + (URL(string: "file:///")!, "file:///"), + (URL(string: "http://example.com")!, "http://example.com"), + ] + + func testAttribute() { + let decoder = XMLDecoder() + let encoder = XMLEncoder() + + encoder.nodeEncodingStrategy = .custom { codableType, _ in + return { _ in .attribute } + } + + for (value, xmlString) in values { + do { + let xmlString = +""" + +""" + let xmlData = xmlString.data(using: .utf8)! + + let decoded = try decoder.decode(Container.self, from: xmlData) + XCTAssertEqual(decoded.value, value) + + let encoded = try encoder.encode(decoded, withRootKey: "container") + XCTAssertEqual(String(data: encoded, encoding: .utf8)!, xmlString) + } catch { + XCTAssert(false, "failed to decode test xml: \(error)") + } + } + } + + func testElement() { + let decoder = XMLDecoder() + let encoder = XMLEncoder() + + encoder.outputFormatting = [.prettyPrinted] + + for (value, xmlString) in values { + do { + let xmlString = +""" + + \(xmlString) + +""" + let xmlData = xmlString.data(using: .utf8)! + + let decoded = try decoder.decode(Container.self, from: xmlData) + XCTAssertEqual(decoded.value, value) + + let encoded = try encoder.encode(decoded, withRootKey: "container") + XCTAssertEqual(String(data: encoded, encoding: .utf8)!, xmlString) + } catch { + XCTAssert(false, "failed to decode test xml: \(error)") + } + } + } + + static var allTests = [ + ("testAttribute", testAttribute), + ("testElement", testElement), + ] +} diff --git a/XMLCoder.xcodeproj/project.pbxproj b/XMLCoder.xcodeproj/project.pbxproj index 86127444..4a1c79fa 100644 --- a/XMLCoder.xcodeproj/project.pbxproj +++ b/XMLCoder.xcodeproj/project.pbxproj @@ -22,6 +22,9 @@ /* Begin PBXBuildFile section */ 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 */; }; + BF63EF0A21CD7C1A001D38C5 /* URLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF63EF0921CD7C1A001D38C5 /* URLTests.swift */; }; BF9457A821CBB498005ACFDE /* NullBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF94579E21CBB497005ACFDE /* NullBox.swift */; }; BF9457A921CBB498005ACFDE /* KeyedBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF94579F21CBB497005ACFDE /* KeyedBox.swift */; }; BF9457AA21CBB498005ACFDE /* UnkeyedBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9457A021CBB497005ACFDE /* UnkeyedBox.swift */; }; @@ -107,6 +110,9 @@ /* Begin PBXFileReference section */ 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 = ""; }; + BF63EF0921CD7C1A001D38C5 /* URLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTests.swift; sourceTree = ""; }; BF94579E21CBB497005ACFDE /* NullBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NullBox.swift; sourceTree = ""; }; BF94579F21CBB497005ACFDE /* KeyedBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyedBox.swift; sourceTree = ""; }; BF9457A021CBB497005ACFDE /* UnkeyedBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnkeyedBox.swift; sourceTree = ""; }; @@ -215,6 +221,7 @@ BF9457A521CBB498005ACFDE /* StringBox.swift */, BF9457D821CBB5D2005ACFDE /* DataBox.swift */, BF9457D921CBB5D2005ACFDE /* DateBox.swift */, + BF63EF0521CD7A74001D38C5 /* URLBox.swift */, BF9457A021CBB497005ACFDE /* UnkeyedBox.swift */, BF94579F21CBB497005ACFDE /* KeyedBox.swift */, ); @@ -246,6 +253,7 @@ BF9457C421CBB516005ACFDE /* StringBoxTests.swift */, BF9457DE21CBB683005ACFDE /* DataBoxTests.swift */, BF9457DC21CBB62C005ACFDE /* DateBoxTests.swift */, + BF63EF0721CD7AF8001D38C5 /* URLBoxTests.swift */, BF9457C721CBB516005ACFDE /* UnkeyedBoxTests.swift */, BF9457BF21CBB516005ACFDE /* KeyedBoxTests.swift */, ); @@ -266,6 +274,7 @@ BF9457EA21CBB6BC005ACFDE /* DecimalTests.swift */, BF9457EB21CBB6BC005ACFDE /* KeyedTests.swift */, BF9457EC21CBB6BC005ACFDE /* DataTests.swift */, + BF63EF0921CD7C1A001D38C5 /* URLTests.swift */, ); path = Minimal; sourceTree = ""; @@ -453,6 +462,7 @@ BF9457BB21CBB4DB005ACFDE /* XMLKey.swift in Sources */, OBJ_48 /* DecodingErrorExtension.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 */, BF9457D521CBB59E005ACFDE /* UIntBox.swift in Sources */, @@ -499,6 +509,7 @@ BF9457F021CBB6BC005ACFDE /* StringTests.swift in Sources */, BF9457ED21CBB6BC005ACFDE /* BoolTests.swift in Sources */, D1FC040521C7EF8200065B43 /* RJISample.swift in Sources */, + BF63EF0A21CD7C1A001D38C5 /* URLTests.swift in Sources */, BF9457CE21CBB516005ACFDE /* StringBoxTests.swift in Sources */, BF9457D021CBB516005ACFDE /* UIntBoxTests.swift in Sources */, OBJ_80 /* BooksTest.swift in Sources */, @@ -517,6 +528,7 @@ OBJ_87 /* PlantCatalog.swift in Sources */, BF9457C921CBB516005ACFDE /* KeyedBoxTests.swift in Sources */, OBJ_88 /* PlantTest.swift in Sources */, + BF63EF0821CD7AF8001D38C5 /* URLBoxTests.swift in Sources */, BF9457DD21CBB62C005ACFDE /* DateBoxTests.swift in Sources */, BF9457CD21CBB516005ACFDE /* FloatBoxTests.swift in Sources */, BF9457F621CBB6BC005ACFDE /* KeyedTests.swift in Sources */,