From ce52f1d4910b1cc4dab7e7fc60c8ac384297297e Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 11 Dec 2023 14:14:04 +1300 Subject: [PATCH 1/7] Add HTTPRequestBuilder --- WordPressKit.xcodeproj/project.pbxproj | 4 + WordPressKit/HTTPRequestBuilder.swift | 140 +++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 WordPressKit/HTTPRequestBuilder.swift diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 261190c4..0b1b31aa 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -140,6 +140,7 @@ 46ABD0E6262EEDAB00C7FF24 /* FakeInfoDictionaryObjectProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46ABD0E5262EEDAB00C7FF24 /* FakeInfoDictionaryObjectProvider.swift */; }; 46ABD0EA262EEE0400C7FF24 /* AppTransportSecuritySettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46ABD0E9262EEE0400C7FF24 /* AppTransportSecuritySettingsTests.swift */; }; 4A11239A2B19269A004690CF /* WordPressAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1123992B19269A004690CF /* WordPressAPIError.swift */; }; + 4A11239C2B1926B7004690CF /* HTTPRequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A11239B2B1926B7004690CF /* HTTPRequestBuilder.swift */; }; 4A1DEF44293051BC00322608 /* LoggingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A1DEF43293051BC00322608 /* LoggingTests.swift */; }; 4A1DEF46293051C600322608 /* LoggingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 4A1DEF45293051C600322608 /* LoggingTests.m */; }; 4A68E3CD29404181004AC3DC /* RemoteBlog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3CC29404181004AC3DC /* RemoteBlog.swift */; }; @@ -832,6 +833,7 @@ 46ABD0E5262EEDAB00C7FF24 /* FakeInfoDictionaryObjectProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FakeInfoDictionaryObjectProvider.swift; sourceTree = ""; }; 46ABD0E9262EEE0400C7FF24 /* AppTransportSecuritySettingsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppTransportSecuritySettingsTests.swift; sourceTree = ""; }; 4A1123992B19269A004690CF /* WordPressAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordPressAPIError.swift; sourceTree = ""; }; + 4A11239B2B1926B7004690CF /* HTTPRequestBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPRequestBuilder.swift; sourceTree = ""; }; 4A1DEF43293051BC00322608 /* LoggingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggingTests.swift; sourceTree = ""; }; 4A1DEF45293051C600322608 /* LoggingTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LoggingTests.m; sourceTree = ""; }; 4A68E3CC29404181004AC3DC /* RemoteBlog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteBlog.swift; sourceTree = ""; }; @@ -2420,6 +2422,7 @@ 93BD27791EE73944002BB00B /* WordPressOrgXMLRPCApi.swift */, 93BD277A1EE73944002BB00B /* WordPressOrgXMLRPCValidator.swift */, 93BD277B1EE73944002BB00B /* WordPressRSDParser.swift */, + 4A11239B2B1926B7004690CF /* HTTPRequestBuilder.swift */, 4A1123992B19269A004690CF /* WordPressAPIError.swift */, ); name = WordPressAPI; @@ -3314,6 +3317,7 @@ 7397F01A220A072500C723F3 /* ActivityServiceRemote_ApiVersion1_0.swift in Sources */, 4A68E3D429406AA0004AC3DC /* RemoteMenuItem.swift in Sources */, 8B2F4BEF24ACCC120056C08A /* RemoteReaderCard.swift in Sources */, + 4A11239C2B1926B7004690CF /* HTTPRequestBuilder.swift in Sources */, 40E7FEB1220FB3B60032834E /* StatsAnnualAndMostPopularTimeInsight.swift in Sources */, 3F758FD324F6C68200BBA2FC /* AnnouncementServiceRemote.swift in Sources */, FAB4F32324EC072700F259BA /* ReaderPostServiceRemote+Subscriptions.swift in Sources */, diff --git a/WordPressKit/HTTPRequestBuilder.swift b/WordPressKit/HTTPRequestBuilder.swift new file mode 100644 index 00000000..3773e73a --- /dev/null +++ b/WordPressKit/HTTPRequestBuilder.swift @@ -0,0 +1,140 @@ +import Foundation + +final class HTTPRequestBuilder { + enum Method: String { + case get = "GET" + case post = "POST" + case put = "PUT" + case patch = "PATCH" + case delete = "DELETE" + + var allowsHTTPBody: Bool { + self == .post || self == .put || self == .patch + } + } + private var urlComponents: URLComponents + private var method: Method = .get + private var path: String + private var headers: [String: String] = [:] + private var bodyBuilder: ((inout URLRequest) throws -> Void)? + + init(url: URL) { + assert(url.scheme == "http" || url.scheme == "https") + assert(url.host != nil) + + urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)! + path = urlComponents.path + } + + func set(method: Method) -> Self { + self.method = method + return self + } + + func set(path: String) -> Self { + assert(!path.contains("?") && !path.contains("#"), "Path should not have query or fragment: \(path)") + + self.path = path + return self + } + + func set(value: String?, forHeader header: String) -> Self { + headers[header] = value + return self + } + + func query(name: String, value: String?, override: Bool = false) -> Self { + append(query: [URLQueryItem(name: name, value: value)], override: override) + } + + func append(query: [URLQueryItem], override: Bool) -> Self { + urlComponents.queryItems = (urlComponents.queryItems ?? []) + query + return self + } + + func body(form: [String: String]) -> Self { + headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8" + bodyBuilder = { req in + var url = URLComponents(string: "https://wordpress.com")! + url.queryItems = form.map { URLQueryItem(name: $0, value: $1) } + req.httpBody = url.percentEncodedQuery?.data(using: .utf8) + } + return self + } + + func body(json: Encodable, jsonEncoder: JSONEncoder = JSONEncoder()) -> Self { + body(json: { + try jsonEncoder.encode(json) + }) + } + + func body(json: Any) -> Self { + body(json: { + try JSONSerialization.data(withJSONObject: json) + }) + } + + func body(json: @escaping () throws -> Data) -> Self { + headers["Content-Type"] = "application/json; charset=utf-8" + bodyBuilder = { req in + req.httpBody = try json() + } + return self + } + + // FIXME: Not implemented yet + func body(xmlrpc: Any /* XMLRPCRequest */) -> Self { + body(xml: { + fatalError("To be implemented") + }) + } + + // FIXME: Not implemented yet + func appendXMLRPCArgument(value: Any) -> Self { + fatalError("To be implemented") + } + + func body(xml: @escaping () throws -> Data) -> Self { + headers["Content-Type"] = "application/xml; charset=utf-8" + bodyBuilder = { req in + req.httpBody = try xml() + } + return self + } + + func build() throws -> URLRequest { + if path == "" || path.hasPrefix("/") { + urlComponents.path = path + } else { + var newPath = urlComponents.path + if !newPath.hasPrefix("/") { + newPath = "/" + newPath + } + + if newPath.hasSuffix("/") { + newPath = newPath + path + } else { + newPath = newPath + "/" + path + } + urlComponents.path = newPath + } + + guard let url = urlComponents.url else { + throw URLError(.badURL) + } + + var request = URLRequest(url: url) + request.httpMethod = method.rawValue + + for (header, value) in headers { + request.addValue(value, forHTTPHeaderField: header) + } + + if let bodyBuilder { + assert(method.allowsHTTPBody, "Can't include body in HTTP \(method.rawValue) requests") + try bodyBuilder(&request) + } + + return request + } +} From 577ebd4d44d0bbd82fb104e51a73b75767a46344 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Mon, 11 Dec 2023 14:24:28 +1300 Subject: [PATCH 2/7] Add unit tests for HTTPRequestBuilder --- WordPressKit.xcodeproj/project.pbxproj | 4 + .../Utilities/HTTPRequestBuilderTests.swift | 93 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 0b1b31aa..861db606 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -151,6 +151,7 @@ 4A68E3DD294070A7004AC3DC /* RemoteReaderSite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3DC294070A7004AC3DC /* RemoteReaderSite.swift */; }; 4A68E3DF29407100004AC3DC /* RemoteReaderTopic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3DE29407100004AC3DC /* RemoteReaderTopic.swift */; }; 4A68E3E1294076C1004AC3DC /* RemoteReaderSiteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A68E3E0294076C1004AC3DC /* RemoteReaderSiteInfo.swift */; }; + 4A6B4A842B26974F00802316 /* HTTPRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6B4A832B26974F00802316 /* HTTPRequestBuilderTests.swift */; }; 4AA5A1A32AA68F6B00969464 /* MediaLibraryTestSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA5A1A22AA68F6B00969464 /* MediaLibraryTestSupport.swift */; }; 4AA5A1A52AA695D700969464 /* LoadMediaLibraryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AA5A1A42AA695D700969464 /* LoadMediaLibraryTests.swift */; }; 57BCD3D426209D9500292CB3 /* AppTransportSecuritySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57BCD3D326209D9500292CB3 /* AppTransportSecuritySettings.swift */; }; @@ -844,6 +845,7 @@ 4A68E3DC294070A7004AC3DC /* RemoteReaderSite.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteReaderSite.swift; sourceTree = ""; }; 4A68E3DE29407100004AC3DC /* RemoteReaderTopic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteReaderTopic.swift; sourceTree = ""; }; 4A68E3E0294076C1004AC3DC /* RemoteReaderSiteInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteReaderSiteInfo.swift; sourceTree = ""; }; + 4A6B4A832B26974F00802316 /* HTTPRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPRequestBuilderTests.swift; sourceTree = ""; }; 4AA5A1A22AA68F6B00969464 /* MediaLibraryTestSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaLibraryTestSupport.swift; sourceTree = ""; }; 4AA5A1A42AA695D700969464 /* LoadMediaLibraryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMediaLibraryTests.swift; sourceTree = ""; }; 57BCD3D326209D9500292CB3 /* AppTransportSecuritySettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTransportSecuritySettings.swift; sourceTree = ""; }; @@ -2629,6 +2631,7 @@ 803DE81028FFA9C4007D4E9C /* RemoteConfigRemoteTests.swift */, 4A1DEF43293051BC00322608 /* LoggingTests.swift */, 4A1DEF45293051C600322608 /* LoggingTests.m */, + 4A6B4A832B26974F00802316 /* HTTPRequestBuilderTests.swift */, ); path = Utilities; sourceTree = ""; @@ -3452,6 +3455,7 @@ 8B2F4BE924ABC9DC0056C08A /* ReaderPostServiceRemote+CardsTests.swift in Sources */, 40F9880C221ACEEE00B7B369 /* StatsRemoteV2Tests.swift in Sources */, 4625BAEB253E118400C04AAD /* PageLayoutServiceRemoteTests.swift in Sources */, + 4A6B4A842B26974F00802316 /* HTTPRequestBuilderTests.swift in Sources */, 465F88BF263B54EE00F4C950 /* ChecksumUtilTests.swift in Sources */, 826017001F9FD60A00533B6C /* ActivityServiceRemoteTests.swift in Sources */, 93F50A3A1F226BB600B5BEBA /* WordPressComServiceRemoteRestTests.swift in Sources */, diff --git a/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift b/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift new file mode 100644 index 00000000..c79d642e --- /dev/null +++ b/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift @@ -0,0 +1,93 @@ +import Foundation +import XCTest + +@testable import WordPressKit + +class HTTPRequestBuilderTests: XCTestCase { + + func testDefaultMethod() throws { + let request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!).build() + XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org") + XCTAssertEqual(request.httpMethod, "GET") + } + + func testHeader() throws { + let request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) + .set(value: "Foo", forHeader: "X-Header-1") + .set(value: "Bar", forHeader: "X-Header-2") + .build() + XCTAssertEqual(request.value(forHTTPHeaderField: "X-Header-1"), "Foo") + XCTAssertEqual(request.value(forHTTPHeaderField: "X-Header-2"), "Bar") + } + + func testPath() throws { + var request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) + .set(path: "hello/world") + .build() + XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") + + request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) + .set(path: "/hello/world") + .build() + XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") + + request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org/hello")!) + .set(path: "world") + .build() + XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") + + request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org/hello")!) + .set(path: "/world") + .build() + XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/world") + } + + func testJSONBody() throws { + var request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) + .set(method: .post) + .body(json: 42) + .build() + XCTAssertTrue(request.value(forHTTPHeaderField: "Content-Type")?.contains("application/json") == true) + try XCTAssertEqual(XCTUnwrap(request.httpBodyText), "42") + + request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) + .set(method: .post) + .body(json: ["foo": "bar"]) + .build() + try XCTAssertEqual(XCTUnwrap(request.httpBodyText), #"{"foo":"bar"}"#) + } + + func testJSONBodyWithEncodable() throws { + struct Body: Encodable { + var foo: String + } + let body = Body(foo: "bar") + + let request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) + .set(method: .post) + .body(json: body) + .build() + XCTAssertTrue(request.value(forHTTPHeaderField: "Content-Type")?.contains("application/json") == true) + try XCTAssertEqual(XCTUnwrap(request.httpBodyText), #"{"foo":"bar"}"#) + } + + func testFormBody() throws { + let request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) + .set(method: .post) + .body(form: ["name": "Foo Bar"]) + .build() + XCTAssertTrue(request.value(forHTTPHeaderField: "Content-Type")?.contains("application/x-www-form-urlencoded") == true) + try XCTAssertEqual(XCTUnwrap(request.httpBodyText), #"name=Foo%20Bar"#) + } + +} + +private extension URLRequest { + var httpBodyText: String? { + guard let data = httpBody else { + return nil + } + + return String(data: data, encoding: .utf8) + } +} From b52e98d572230e251ee70c2d997611b18df1997d Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 12 Dec 2023 09:46:00 +1300 Subject: [PATCH 3/7] Rename a few methods in HTTPRequestBuilder --- WordPressKit/HTTPRequestBuilder.swift | 9 +++++---- .../Utilities/HTTPRequestBuilderTests.swift | 20 +++++++++---------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/WordPressKit/HTTPRequestBuilder.swift b/WordPressKit/HTTPRequestBuilder.swift index 3773e73a..0408ee81 100644 --- a/WordPressKit/HTTPRequestBuilder.swift +++ b/WordPressKit/HTTPRequestBuilder.swift @@ -12,6 +12,7 @@ final class HTTPRequestBuilder { self == .post || self == .put || self == .patch } } + private var urlComponents: URLComponents private var method: Method = .get private var path: String @@ -26,20 +27,20 @@ final class HTTPRequestBuilder { path = urlComponents.path } - func set(method: Method) -> Self { + func method(_ method: Method) -> Self { self.method = method return self } - func set(path: String) -> Self { + func path(_ path: String) -> Self { assert(!path.contains("?") && !path.contains("#"), "Path should not have query or fragment: \(path)") self.path = path return self } - func set(value: String?, forHeader header: String) -> Self { - headers[header] = value + func header(name: String, value: String?) -> Self { + headers[name] = value return self } diff --git a/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift b/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift index c79d642e..6314d835 100644 --- a/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift +++ b/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift @@ -13,8 +13,8 @@ class HTTPRequestBuilderTests: XCTestCase { func testHeader() throws { let request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) - .set(value: "Foo", forHeader: "X-Header-1") - .set(value: "Bar", forHeader: "X-Header-2") + .header(name: "X-Header-1", value: "Foo") + .header(name: "X-Header-2", value: "Bar") .build() XCTAssertEqual(request.value(forHTTPHeaderField: "X-Header-1"), "Foo") XCTAssertEqual(request.value(forHTTPHeaderField: "X-Header-2"), "Bar") @@ -22,36 +22,36 @@ class HTTPRequestBuilderTests: XCTestCase { func testPath() throws { var request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) - .set(path: "hello/world") + .path("hello/world") .build() XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) - .set(path: "/hello/world") + .path("/hello/world") .build() XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org/hello")!) - .set(path: "world") + .path("world") .build() XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org/hello")!) - .set(path: "/world") + .path("/world") .build() XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/world") } func testJSONBody() throws { var request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) - .set(method: .post) + .method(.post) .body(json: 42) .build() XCTAssertTrue(request.value(forHTTPHeaderField: "Content-Type")?.contains("application/json") == true) try XCTAssertEqual(XCTUnwrap(request.httpBodyText), "42") request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) - .set(method: .post) + .method(.post) .body(json: ["foo": "bar"]) .build() try XCTAssertEqual(XCTUnwrap(request.httpBodyText), #"{"foo":"bar"}"#) @@ -64,7 +64,7 @@ class HTTPRequestBuilderTests: XCTestCase { let body = Body(foo: "bar") let request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) - .set(method: .post) + .method(.post) .body(json: body) .build() XCTAssertTrue(request.value(forHTTPHeaderField: "Content-Type")?.contains("application/json") == true) @@ -73,7 +73,7 @@ class HTTPRequestBuilderTests: XCTestCase { func testFormBody() throws { let request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) - .set(method: .post) + .method(.post) .body(form: ["name": "Foo Bar"]) .build() XCTAssertTrue(request.value(forHTTPHeaderField: "Content-Type")?.contains("application/x-www-form-urlencoded") == true) From c70478bba7613d9b8867f06b8d31db12bc9126cb Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 12 Dec 2023 09:52:22 +1300 Subject: [PATCH 4/7] Use isEmpty Co-authored-by: Gio Lodi --- WordPressKit/HTTPRequestBuilder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPressKit/HTTPRequestBuilder.swift b/WordPressKit/HTTPRequestBuilder.swift index 0408ee81..ddd2dc97 100644 --- a/WordPressKit/HTTPRequestBuilder.swift +++ b/WordPressKit/HTTPRequestBuilder.swift @@ -104,7 +104,7 @@ final class HTTPRequestBuilder { } func build() throws -> URLRequest { - if path == "" || path.hasPrefix("/") { + if path.isEmpty || path.hasPrefix("/") { urlComponents.path = path } else { var newPath = urlComponents.path From e27fe437c502489ae477df4a79d0a2c5801dcd54 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 12 Dec 2023 09:56:34 +1300 Subject: [PATCH 5/7] More tests around URL and HTTP methods --- .../Utilities/HTTPRequestBuilderTests.swift | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift b/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift index 6314d835..904c0172 100644 --- a/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift +++ b/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift @@ -5,10 +5,26 @@ import XCTest class HTTPRequestBuilderTests: XCTestCase { - func testDefaultMethod() throws { - let request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!).build() - XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org") - XCTAssertEqual(request.httpMethod, "GET") + func testURL() throws { + try XCTAssertEqual(HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!).build().url?.absoluteString, "https://wordpress.org") + try XCTAssertEqual(HTTPRequestBuilder(url: URL(string: "https://wordpress.com")!).build().url?.absoluteString, "https://wordpress.com") + } + + func testHTTPMethods() throws { + try XCTAssertEqual(HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!).build().httpMethod, "GET") + XCTAssertFalse(HTTPRequestBuilder.Method.get.allowsHTTPBody) + + try XCTAssertEqual(HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!).method(.delete).build().httpMethod, "DELETE") + XCTAssertFalse(HTTPRequestBuilder.Method.delete.allowsHTTPBody) + + try XCTAssertEqual(HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!).method(.post).build().httpMethod, "POST") + XCTAssertTrue(HTTPRequestBuilder.Method.post.allowsHTTPBody) + + try XCTAssertEqual(HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!).method(.patch).build().httpMethod, "PATCH") + XCTAssertTrue(HTTPRequestBuilder.Method.patch.allowsHTTPBody) + + try XCTAssertEqual(HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!).method(.put).build().httpMethod, "PUT") + XCTAssertTrue(HTTPRequestBuilder.Method.put.allowsHTTPBody) } func testHeader() throws { From fbbbd0c9fb24c5255023f9f5bfae3913cfdac426 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 12 Dec 2023 09:57:48 +1300 Subject: [PATCH 6/7] Move a couple of helper methods into an extension --- WordPressKit/HTTPRequestBuilder.swift | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/WordPressKit/HTTPRequestBuilder.swift b/WordPressKit/HTTPRequestBuilder.swift index ddd2dc97..eb9e1941 100644 --- a/WordPressKit/HTTPRequestBuilder.swift +++ b/WordPressKit/HTTPRequestBuilder.swift @@ -83,18 +83,6 @@ final class HTTPRequestBuilder { return self } - // FIXME: Not implemented yet - func body(xmlrpc: Any /* XMLRPCRequest */) -> Self { - body(xml: { - fatalError("To be implemented") - }) - } - - // FIXME: Not implemented yet - func appendXMLRPCArgument(value: Any) -> Self { - fatalError("To be implemented") - } - func body(xml: @escaping () throws -> Data) -> Self { headers["Content-Type"] = "application/xml; charset=utf-8" bodyBuilder = { req in @@ -139,3 +127,17 @@ final class HTTPRequestBuilder { return request } } + +extension HTTPRequestBuilder { + // FIXME: Not implemented yet + func body(xmlrpc: Any /* XMLRPCRequest */) -> Self { + body(xml: { + fatalError("To be implemented") + }) + } + + // FIXME: Not implemented yet + func appendXMLRPCArgument(value: Any) -> Self { + fatalError("To be implemented") + } +} From 4bb836093c79c8ee7c897afe42ded12032766f01 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Tue, 12 Dec 2023 10:41:25 +1300 Subject: [PATCH 7/7] Only allow appending path to the initial URL --- WordPressKit/HTTPRequestBuilder.swift | 32 +++++++------------ .../Utilities/HTTPRequestBuilderTests.swift | 10 +++--- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/WordPressKit/HTTPRequestBuilder.swift b/WordPressKit/HTTPRequestBuilder.swift index eb9e1941..045b7089 100644 --- a/WordPressKit/HTTPRequestBuilder.swift +++ b/WordPressKit/HTTPRequestBuilder.swift @@ -15,7 +15,6 @@ final class HTTPRequestBuilder { private var urlComponents: URLComponents private var method: Method = .get - private var path: String private var headers: [String: String] = [:] private var bodyBuilder: ((inout URLRequest) throws -> Void)? @@ -24,7 +23,6 @@ final class HTTPRequestBuilder { assert(url.host != nil) urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)! - path = urlComponents.path } func method(_ method: Method) -> Self { @@ -32,10 +30,20 @@ final class HTTPRequestBuilder { return self } - func path(_ path: String) -> Self { + func append(path: String) -> Self { assert(!path.contains("?") && !path.contains("#"), "Path should not have query or fragment: \(path)") - self.path = path + var relPath = path + if relPath.hasPrefix("/") { + _ = relPath.removeFirst() + } + + if urlComponents.path.hasSuffix("/") { + urlComponents.path = urlComponents.path.appending(relPath) + } else { + urlComponents.path = urlComponents.path.appending("/").appending(relPath) + } + return self } @@ -92,22 +100,6 @@ final class HTTPRequestBuilder { } func build() throws -> URLRequest { - if path.isEmpty || path.hasPrefix("/") { - urlComponents.path = path - } else { - var newPath = urlComponents.path - if !newPath.hasPrefix("/") { - newPath = "/" + newPath - } - - if newPath.hasSuffix("/") { - newPath = newPath + path - } else { - newPath = newPath + "/" + path - } - urlComponents.path = newPath - } - guard let url = urlComponents.url else { throw URLError(.badURL) } diff --git a/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift b/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift index 904c0172..c1667a7e 100644 --- a/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift +++ b/WordPressKitTests/Utilities/HTTPRequestBuilderTests.swift @@ -38,24 +38,24 @@ class HTTPRequestBuilderTests: XCTestCase { func testPath() throws { var request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) - .path("hello/world") + .append(path: "hello/world") .build() XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org")!) - .path("/hello/world") + .append(path: "/hello/world") .build() XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org/hello")!) - .path("world") + .append(path: "world") .build() XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") request = try HTTPRequestBuilder(url: URL(string: "https://wordpress.org/hello")!) - .path("/world") + .append(path: "/world") .build() - XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/world") + XCTAssertEqual(request.url?.absoluteString, "https://wordpress.org/hello/world") } func testJSONBody() throws {