Merge pull request #1299 from bubski/codable-urlcomponents

diff --git a/CoreFoundation/URL.subproj/CFURLComponents.c b/CoreFoundation/URL.subproj/CFURLComponents.c
index 5756ea8..86dd9f1 100644
--- a/CoreFoundation/URL.subproj/CFURLComponents.c
+++ b/CoreFoundation/URL.subproj/CFURLComponents.c
@@ -556,11 +556,11 @@
         components->_pathComponentValid = true;
     }
     if (!components->_pathComponent) {
-        result = CFSTR("");
+        result = CFStringCreateCopy(kCFAllocatorSystemDefault, CFSTR(""));
     } else {
         result = _CFStringCreateByRemovingPercentEncoding(kCFAllocatorSystemDefault, components->_pathComponent);
         if (!result) {
-            result = CFSTR("");
+            result = CFStringCreateCopy(kCFAllocatorSystemDefault, CFSTR(""));
         }
     }
     __CFUnlock(&components->_lock);
@@ -745,7 +745,7 @@
     result = components->_pathComponent ? CFRetain(components->_pathComponent) : NULL;
     __CFUnlock(&components->_lock);
     
-    if (!result) result = CFSTR("");
+    if (!result) result = CFStringCreateCopy(kCFAllocatorSystemDefault, CFSTR(""));
     
     return ( result );
 }
diff --git a/Foundation/URLComponents.swift b/Foundation/URLComponents.swift
index 281865f..53f2384 100644
--- a/Foundation/URLComponents.swift
+++ b/Foundation/URLComponents.swift
@@ -107,8 +107,7 @@
     /// The getter for this property removes any percent encoding this component may have (if the component allows percent encoding). Setting this property assumes the subcomponent or component string is not percent encoded and will add percent encoding (if the component allows percent encoding).
     public var path: String {
         get {
-            guard let result = _handle.map({ $0.path }) else { return "" }
-            return result
+            return _handle.map { $0.path } ?? ""
         }
         set {
             _applyMutation { $0.path = newValue }
@@ -161,8 +160,7 @@
     /// The getter for this property retains any percent encoding this component may have. Setting this properties assumes the component string is already correctly percent encoded. Attempting to set an incorrectly percent encoded string will cause a `fatalError`. Although ';' is a legal path character, it is recommended that it be percent-encoded for best compatibility with `URL` (`String.addingPercentEncoding(withAllowedCharacters:)` will percent-encode any ';' characters if you pass `CharacterSet.urlPathAllowed`).
     public var percentEncodedPath: String {
         get {
-            guard let result = _handle.map({ $0.percentEncodedPath }) else { return "" }
-            return result
+            return _handle.map { $0.percentEncodedPath } ?? ""
         }
         set {
             _applyMutation { $0.percentEncodedPath = newValue }
@@ -456,3 +454,42 @@
         return result!
     }
 }
+
+extension URLComponents : Codable {
+    private enum CodingKeys : Int, CodingKey {
+        case scheme
+        case user
+        case password
+        case host
+        case port
+        case path
+        case query
+        case fragment
+    }
+
+    public init(from decoder: Decoder) throws {
+        self.init()
+
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+        self.scheme = try container.decodeIfPresent(String.self, forKey: .scheme)
+        self.user = try container.decodeIfPresent(String.self, forKey: .user)
+        self.password = try container.decodeIfPresent(String.self, forKey: .password)
+        self.host = try container.decodeIfPresent(String.self, forKey: .host)
+        self.port = try container.decodeIfPresent(Int.self, forKey: .port)
+        self.path = try container.decode(String.self, forKey: .path)
+        self.query = try container.decodeIfPresent(String.self, forKey: .query)
+        self.fragment = try container.decodeIfPresent(String.self, forKey: .fragment)
+    }
+
+    public func encode(to encoder: Encoder) throws {
+        var container = encoder.container(keyedBy: CodingKeys.self)
+        try container.encodeIfPresent(self.scheme, forKey: .scheme)
+        try container.encodeIfPresent(self.user, forKey: .user)
+        try container.encodeIfPresent(self.password, forKey: .password)
+        try container.encodeIfPresent(self.host, forKey: .host)
+        try container.encodeIfPresent(self.port, forKey: .port)
+        try container.encode(self.path, forKey: .path)
+        try container.encodeIfPresent(self.query, forKey: .query)
+        try container.encodeIfPresent(self.fragment, forKey: .fragment)
+    }
+}
diff --git a/TestFoundation/TestCodable.swift b/TestFoundation/TestCodable.swift
index 1e24f5f..63d8271 100644
--- a/TestFoundation/TestCodable.swift
+++ b/TestFoundation/TestCodable.swift
@@ -472,6 +472,103 @@
             XCTFail("\(error)")
         }
     }
+
+    // MARK: - URLComponents
+    lazy var urlComponentsValues: [URLComponents] = [
+        URLComponents(),
+
+        URLComponents(string: "http://swift.org")!,
+        URLComponents(string: "http://swift.org:80")!,
+        URLComponents(string: "https://www.mywebsite.org/api/v42/something.php#param1=hi&param2=hello")!,
+        URLComponents(string: "ftp://johnny:apples@myftpserver.org:4242/some/path")!,
+
+        URLComponents(url: URL(string: "http://swift.org")!, resolvingAgainstBaseURL: false)!,
+        URLComponents(url: URL(string: "http://swift.org:80")!, resolvingAgainstBaseURL: false)!,
+        URLComponents(url: URL(string: "https://www.mywebsite.org/api/v42/something.php#param1=hi&param2=hello")!, resolvingAgainstBaseURL: false)!,
+        URLComponents(url: URL(string: "ftp://johnny:apples@myftpserver.org:4242/some/path")!, resolvingAgainstBaseURL: false)!,
+        URLComponents(url: URL(fileURLWithPath: NSTemporaryDirectory()), resolvingAgainstBaseURL: false)!,
+        URLComponents(url: URL(fileURLWithPath: "/"), resolvingAgainstBaseURL: false)!,
+        URLComponents(url: URL(string: "documentation", relativeTo: URL(string: "http://swift.org")!)!, resolvingAgainstBaseURL: false)!,
+
+        URLComponents(url: URL(string: "http://swift.org")!, resolvingAgainstBaseURL: true)!,
+        URLComponents(url: URL(string: "http://swift.org:80")!, resolvingAgainstBaseURL: true)!,
+        URLComponents(url: URL(string: "https://www.mywebsite.org/api/v42/something.php#param1=hi&param2=hello")!, resolvingAgainstBaseURL: true)!,
+        URLComponents(url: URL(string: "ftp://johnny:apples@myftpserver.org:4242/some/path")!, resolvingAgainstBaseURL: true)!,
+        URLComponents(url: URL(fileURLWithPath: NSTemporaryDirectory()), resolvingAgainstBaseURL: true)!,
+        URLComponents(url: URL(fileURLWithPath: "/"), resolvingAgainstBaseURL: true)!,
+        URLComponents(url: URL(string: "documentation", relativeTo: URL(string: "http://swift.org")!)!, resolvingAgainstBaseURL: true)!,
+
+        {
+            var components = URLComponents()
+            components.scheme = "https"
+            return components
+        }(),
+
+        {
+            var components = URLComponents()
+            components.user = "johnny"
+            return components
+        }(),
+
+        {
+            var components = URLComponents()
+            components.password = "apples"
+            return components
+        }(),
+
+        {
+            var components = URLComponents()
+            components.host = "0.0.0.0"
+            return components
+        }(),
+
+        {
+            var components = URLComponents()
+            components.port = 8080
+            return components
+        }(),
+
+        {
+            var components = URLComponents()
+            components.path = ".."
+            return components
+        }(),
+
+        {
+            var components = URLComponents()
+            components.query = "param1=hi&param2=there"
+            return components
+        }(),
+
+        {
+            var components = URLComponents()
+            components.fragment = "anchor"
+            return components
+        }(),
+
+        {
+            var components = URLComponents()
+            components.scheme = "ftp"
+            components.user = "johnny"
+            components.password = "apples"
+            components.host = "0.0.0.0"
+            components.port = 4242
+            components.path = "/some/file"
+            components.query = "utf8=✅"
+            components.fragment = "anchor"
+            return components
+        }()
+    ]
+
+    func test_URLComponents_JSON() {
+        for (components) in urlComponentsValues {
+            do {
+                try expectRoundTripEqualityThroughJSON(for: components)
+            } catch let error {
+                XCTFail("\(error)")
+            }
+        }
+    }
 }
 
 extension TestCodable {
@@ -494,6 +591,7 @@
             ("test_Calendar_JSON", test_Calendar_JSON),
             ("test_DateComponents_JSON", test_DateComponents_JSON),
             ("test_Measurement_JSON", test_Measurement_JSON),
+            ("test_URLComponents_JSON", test_URLComponents_JSON),
         ]
     }
 }
diff --git a/TestFoundation/TestURL.swift b/TestFoundation/TestURL.swift
index 0a9b652..f84f06c 100644
--- a/TestFoundation/TestURL.swift
+++ b/TestFoundation/TestURL.swift
@@ -523,7 +523,9 @@
             ("test_port", test_portSetter),
             ("test_url", test_url),
             ("test_copy", test_copy),
-            ("test_createURLWithComponents", test_createURLWithComponents)
+            ("test_createURLWithComponents", test_createURLWithComponents),
+            ("test_path", test_path),
+            ("test_percentEncodedPath", test_percentEncodedPath),
         ]
     }
     
@@ -617,4 +619,43 @@
         XCTAssertEqual(urlComponents.queryItems?.count, 4)
     }
 
+    func test_path() {
+        let c1 = URLComponents()
+        XCTAssertEqual(c1.path, "")
+
+        let c2 = URLComponents(string: "http://swift.org")
+        XCTAssertEqual(c2?.path, "")
+
+        let c3 = URLComponents(string: "http://swift.org/")
+        XCTAssertEqual(c3?.path, "/")
+
+        let c4 = URLComponents(string: "http://swift.org/foo/bar")
+        XCTAssertEqual(c4?.path, "/foo/bar")
+
+        let c5 = URLComponents(string: "http://swift.org:80/foo/bar")
+        XCTAssertEqual(c5?.path, "/foo/bar")
+
+        let c6 = URLComponents(string: "http://swift.org:80/foo/b%20r")
+        XCTAssertEqual(c6?.path, "/foo/b r")
+    }
+
+    func test_percentEncodedPath() {
+        let c1 = URLComponents()
+        XCTAssertEqual(c1.percentEncodedPath, "")
+
+        let c2 = URLComponents(string: "http://swift.org")
+        XCTAssertEqual(c2?.percentEncodedPath, "")
+
+        let c3 = URLComponents(string: "http://swift.org/")
+        XCTAssertEqual(c3?.percentEncodedPath, "/")
+
+        let c4 = URLComponents(string: "http://swift.org/foo/bar")
+        XCTAssertEqual(c4?.percentEncodedPath, "/foo/bar")
+
+        let c5 = URLComponents(string: "http://swift.org:80/foo/bar")
+        XCTAssertEqual(c5?.percentEncodedPath, "/foo/bar")
+
+        let c6 = URLComponents(string: "http://swift.org:80/foo/b%20r")
+        XCTAssertEqual(c6?.percentEncodedPath, "/foo/b%20r")
+    }
 }