Skip to content

Commit

Permalink
Extracted URL coding logic into URLBox with corresponding unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
regexident authored and MaxDesiatov committed Dec 21, 2018
1 parent a6a7233 commit d21bb2e
Show file tree
Hide file tree
Showing 6 changed files with 234 additions and 31 deletions.
53 changes: 53 additions & 0 deletions 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
}
}
29 changes: 17 additions & 12 deletions Sources/XMLCoder/Decoder/XMLDecoder.swift
Expand Up @@ -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<T: Decodable>(_ box: Box) throws -> T? {
let decoded: T
Expand All @@ -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
Expand Down
29 changes: 10 additions & 19 deletions Sources/XMLCoder/Encoder/XMLEncoder.swift
Expand Up @@ -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?

Expand Down Expand Up @@ -552,22 +547,18 @@ extension _XMLEncoder {
}
}

func box<T : Encodable>(_ 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<T: Encodable>(_ value: T) throws -> Box? {

internal func box<T: Encodable>(_ 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)
}

Expand All @@ -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()
Expand Down
61 changes: 61 additions & 0 deletions 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)
}
}
}
81 changes: 81 additions & 0 deletions 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 =
"""
<container value="\(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 =
"""
<container>
<value>\(xmlString)</value>
</container>
"""
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),
]
}
12 changes: 12 additions & 0 deletions XMLCoder.xcodeproj/project.pbxproj
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -107,6 +110,9 @@

/* Begin PBXFileReference section */
BF63EEFF21CCDED2001D38C5 /* XMLStackParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XMLStackParserTests.swift; sourceTree = "<group>"; };
BF63EF0521CD7A74001D38C5 /* URLBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBox.swift; sourceTree = "<group>"; };
BF63EF0721CD7AF8001D38C5 /* URLBoxTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLBoxTests.swift; sourceTree = "<group>"; };
BF63EF0921CD7C1A001D38C5 /* URLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTests.swift; sourceTree = "<group>"; };
BF94579E21CBB497005ACFDE /* NullBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NullBox.swift; sourceTree = "<group>"; };
BF94579F21CBB497005ACFDE /* KeyedBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyedBox.swift; sourceTree = "<group>"; };
BF9457A021CBB497005ACFDE /* UnkeyedBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnkeyedBox.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -215,6 +221,7 @@
BF9457A521CBB498005ACFDE /* StringBox.swift */,
BF9457D821CBB5D2005ACFDE /* DataBox.swift */,
BF9457D921CBB5D2005ACFDE /* DateBox.swift */,
BF63EF0521CD7A74001D38C5 /* URLBox.swift */,
BF9457A021CBB497005ACFDE /* UnkeyedBox.swift */,
BF94579F21CBB497005ACFDE /* KeyedBox.swift */,
);
Expand Down Expand Up @@ -246,6 +253,7 @@
BF9457C421CBB516005ACFDE /* StringBoxTests.swift */,
BF9457DE21CBB683005ACFDE /* DataBoxTests.swift */,
BF9457DC21CBB62C005ACFDE /* DateBoxTests.swift */,
BF63EF0721CD7AF8001D38C5 /* URLBoxTests.swift */,
BF9457C721CBB516005ACFDE /* UnkeyedBoxTests.swift */,
BF9457BF21CBB516005ACFDE /* KeyedBoxTests.swift */,
);
Expand All @@ -266,6 +274,7 @@
BF9457EA21CBB6BC005ACFDE /* DecimalTests.swift */,
BF9457EB21CBB6BC005ACFDE /* KeyedTests.swift */,
BF9457EC21CBB6BC005ACFDE /* DataTests.swift */,
BF63EF0921CD7C1A001D38C5 /* URLTests.swift */,
);
path = Minimal;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down

0 comments on commit d21bb2e

Please sign in to comment.