diff --git a/CHANGELOG.md b/CHANGELOG.md index c4a2b624..9903cc1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ _None._ ### Breaking Changes - Rewrite `WordPressOrgRestApi` to support self hosted sites and WordPress.com sites. [#724] +- Decouple `PluginDirectoryServiceRemote` from Alamofire. [#725] +- Remove `Endpoint`. [#725] ### New Features diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index 078cd8b5..f5839ed0 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -607,7 +607,6 @@ E14694031F344F71004052C8 /* site-plugins-error.json in Resources */ = {isa = PBXBuildFile; fileRef = E14694021F344F71004052C8 /* site-plugins-error.json */; }; E1787DB0200E564B004CB3AF /* timezones.json in Resources */ = {isa = PBXBuildFile; fileRef = E1787DAF200E564B004CB3AF /* timezones.json */; }; E1787DB2200E5690004CB3AF /* TimeZoneServiceRemoteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1787DB1200E5690004CB3AF /* TimeZoneServiceRemoteTests.swift */; }; - E182BF6A1FD961810001D850 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = E182BF691FD961810001D850 /* Endpoint.swift */; }; E194CB731FBDEF6500B0A8B8 /* PluginState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E194CB721FBDEF6400B0A8B8 /* PluginState.swift */; }; E1A6605F1FD694ED00BAC339 /* PluginDirectoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1A6605E1FD694ED00BAC339 /* PluginDirectoryEntry.swift */; }; E1BD95151FD5A2B800CD5CE3 /* PluginDirectoryServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BD95141FD5A2B800CD5CE3 /* PluginDirectoryServiceRemote.swift */; }; @@ -1336,7 +1335,6 @@ E14694021F344F71004052C8 /* site-plugins-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-plugins-error.json"; sourceTree = ""; }; E1787DAF200E564B004CB3AF /* timezones.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = timezones.json; sourceTree = ""; }; E1787DB1200E5690004CB3AF /* TimeZoneServiceRemoteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneServiceRemoteTests.swift; sourceTree = ""; }; - E182BF691FD961810001D850 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; E194CB721FBDEF6400B0A8B8 /* PluginState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PluginState.swift; sourceTree = ""; }; E1A6605E1FD694ED00BAC339 /* PluginDirectoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginDirectoryEntry.swift; sourceTree = ""; }; E1BD95141FD5A2B800CD5CE3 /* PluginDirectoryServiceRemote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginDirectoryServiceRemote.swift; sourceTree = ""; }; @@ -1985,7 +1983,6 @@ 74BA04F11F06DC0A00ED5CD8 /* CommentServiceRemoteXMLRPC.m */, 8BB5F62027A99A2000B2FFAF /* DashboardServiceRemote.swift */, 7E0D64FE22D855700092AD10 /* EditorServiceRemote.swift */, - E182BF691FD961810001D850 /* Endpoint.swift */, F9E56DF724EB125600916770 /* FeatureFlagRemote.swift */, 74650F711F0EA1A700188EDB /* GravatarServiceRemote.swift */, 1769DEA924729AFF00F42EFC /* HomepageSettingsServiceRemote.swift */, @@ -3462,7 +3459,6 @@ F1BB7806240FB90B0030ADDC /* AtomicAuthenticationServiceRemote.swift in Sources */, 404057CE221C38130060250C /* StatsTopVideosTimeIntervalData.swift in Sources */, 7E0D64FF22D855700092AD10 /* EditorServiceRemote.swift in Sources */, - E182BF6A1FD961810001D850 /* Endpoint.swift in Sources */, 9AF4F2FF2183346B00570E4B /* RemoteRevision.swift in Sources */, 17D936252475D8AB008B2205 /* RemoteHomepageType.swift in Sources */, 74BA04F41F06DC0A00ED5CD8 /* CommentServiceRemoteREST.m in Sources */, diff --git a/WordPressKit/Endpoint.swift b/WordPressKit/Endpoint.swift deleted file mode 100644 index 192e310d..00000000 --- a/WordPressKit/Endpoint.swift +++ /dev/null @@ -1,55 +0,0 @@ -import Foundation -import Alamofire - -/// Represents a specific type of Network request. -/// -/// This protocol provides the building blocks to define network requests -/// independent of the network library used. It has 3 responsibilities: -/// -/// - Creating a URLRequest -/// - Validating the response -/// - Parsing response data into the expected result -/// -/// Validation is an optional step that allows inspection of the URLResponse -/// object. If an endpoint doesn’t need custom validation, it can have an empty -/// implementation. -/// -public protocol Endpoint { - associatedtype Output - func buildRequest() throws -> URLRequest - func parseResponse(data: Data) throws -> Output - - /// Validates a response. - /// - /// If the endpoint doesn't need validation, implement this as an empty method. - /// Otherwise, inspect the arguments and throw an error if necessary. - func validate(request: URLRequest?, response: HTTPURLResponse, data: Data?) throws -} - -extension Endpoint { - func request(completion: @escaping (Result) -> Void) { - do { - let request = try buildRequest() - - Alamofire - .request(request) - .validate() - .validate({ (request, response, data) in - do { - try self.validate(request: request, response: response, data: data) - return .success - } catch { - return .failure(error) - } - }) - .responseData(queue: DispatchQueue.global(qos: .utility), completionHandler: { (response) in - let result = response.result.flatMap(self.parseResponse(data:)) - DispatchQueue.main.async { - completion(result) - } - }) - } catch { - completion(.failure(error)) - } - } -} diff --git a/WordPressKit/PluginDirectoryServiceRemote.swift b/WordPressKit/PluginDirectoryServiceRemote.swift index b4bb1a4a..5dd800b7 100644 --- a/WordPressKit/PluginDirectoryServiceRemote.swift +++ b/WordPressKit/PluginDirectoryServiceRemote.swift @@ -1,5 +1,4 @@ import Foundation -import Alamofire private struct PluginDirectoryRemoteConstants { static let dateFormatter: DateFormatter = { @@ -47,7 +46,7 @@ public enum PluginDirectoryFeedType: Hashable { } } -public struct PluginDirectoryGetInformationEndpoint: Endpoint { +public struct PluginDirectoryGetInformationEndpoint { public enum Error: Swift.Error { case pluginNotFound } @@ -57,20 +56,18 @@ public struct PluginDirectoryGetInformationEndpoint: Endpoint { self.slug = slug } - public func buildRequest() throws -> URLRequest { - let url = PluginDirectoryRemoteConstants.getInformationEndpoint - .appendingPathComponent(slug) - .appendingPathExtension("json") - let request = URLRequest(url: url) - let encodedRequest = try URLEncoding.default.encode(request, with: ["fields": "icons,banners"]) - return encodedRequest + func buildRequest() throws -> URLRequest { + try HTTPRequestBuilder(url: PluginDirectoryRemoteConstants.getInformationEndpoint) + .append(percentEncodedPath: "\(slug).json") + .query(name: "fields", value: "icons,banners") + .build() } - public func parseResponse(data: Data) throws -> PluginDirectoryEntry { + func parseResponse(data: Data) throws -> PluginDirectoryEntry { return try PluginDirectoryRemoteConstants.jsonDecoder.decode(PluginDirectoryEntry.self, from: data) } - public func validate(request: URLRequest?, response: HTTPURLResponse, data: Data?) throws { + func validate(response: HTTPURLResponse, data: Data?) throws { // api.wordpress.org has an odd way of responding to plugin info requests for // plugins not in the directory: it will return `null` with an HTTP 200 OK. // This turns that case into a `.pluginNotFound` error. @@ -83,7 +80,7 @@ public struct PluginDirectoryGetInformationEndpoint: Endpoint { } } -public struct PluginDirectoryFeedEndpoint: Endpoint { +public struct PluginDirectoryFeedEndpoint { public enum Error: Swift.Error { case genericError } @@ -91,12 +88,12 @@ public struct PluginDirectoryFeedEndpoint: Endpoint { let feedType: PluginDirectoryFeedType let pageNumber: Int - public init(feedType: PluginDirectoryFeedType) { + init(feedType: PluginDirectoryFeedType) { self.feedType = feedType self.pageNumber = 1 } - public func buildRequest() throws -> URLRequest { + func buildRequest() throws -> URLRequest { var parameters: [String: Any] = ["action": "query_plugins", "request[per_page]": PluginDirectoryRemoteConstants.pluginsPerPage, "request[fields][icons]": 1, @@ -113,17 +110,16 @@ public struct PluginDirectoryFeedEndpoint: Endpoint { } - let request = URLRequest(url: PluginDirectoryRemoteConstants.feedEndpoint) - let encodedRequest = try URLEncoding.default.encode(request, with: parameters) - - return encodedRequest + return try HTTPRequestBuilder(url: PluginDirectoryRemoteConstants.feedEndpoint) + .query(parameters) + .build() } - public func parseResponse(data: Data) throws -> PluginDirectoryFeedPage { + func parseResponse(data: Data) throws -> PluginDirectoryFeedPage { return try PluginDirectoryRemoteConstants.jsonDecoder.decode(PluginDirectoryFeedPage.self, from: data) } - public func validate(request: URLRequest?, response: HTTPURLResponse, data: Data?) throws { + func validate(response: HTTPURLResponse, data: Data?) throws { if response.statusCode != 200 { throw Error.genericError} } } @@ -132,13 +128,19 @@ public struct PluginDirectoryServiceRemote { public init() {} - public func getPluginFeed(_ feedType: PluginDirectoryFeedType, - pageNumber: Int = 1, - completion: @escaping (Result) -> Void) { - PluginDirectoryFeedEndpoint(feedType: feedType).request(completion: completion) + public func getPluginFeed(_ feedType: PluginDirectoryFeedType, pageNumber: Int = 1) async throws -> PluginDirectoryFeedPage { + let endpoint = PluginDirectoryFeedEndpoint(feedType: feedType) + let (data, response) = try await URLSession.shared.data(for: endpoint.buildRequest()) + let httpResponse = response as! HTTPURLResponse + try endpoint.validate(response: httpResponse, data: data) + return try endpoint.parseResponse(data: data) } - public func getPluginInformation(slug: String, completion: @escaping (Result) -> Void) { - PluginDirectoryGetInformationEndpoint(slug: slug).request(completion: completion) + public func getPluginInformation(slug: String) async throws -> PluginDirectoryEntry { + let endpoint = PluginDirectoryGetInformationEndpoint(slug: slug) + let (data, response) = try await URLSession.shared.data(for: endpoint.buildRequest()) + let httpResponse = response as! HTTPURLResponse + try endpoint.validate(response: httpResponse, data: data) + return try endpoint.parseResponse(data: data) } } diff --git a/WordPressKitTests/PluginDirectoryTests.swift b/WordPressKitTests/PluginDirectoryTests.swift index 57288fe1..13b9f1a8 100644 --- a/WordPressKitTests/PluginDirectoryTests.swift +++ b/WordPressKitTests/PluginDirectoryTests.swift @@ -1,4 +1,5 @@ import XCTest +import OHHTTPStubs @testable import WordPressKit class PluginDirectoryTests: XCTestCase { @@ -51,13 +52,33 @@ class PluginDirectoryTests: XCTestCase { } } + func testGetPluginInformation() async throws { + let data = try MockPluginDirectoryProvider.getPluginDirectoryMockData(with: "plugin-directory-rename-xml-rpc", sender: type(of: self)) + stub(condition: isHost("api.wordpress.org")) { _ in + HTTPStubsResponse(data: data, statusCode: 200, headers: ["Content-Type": "application/json"]) + } + + let plugin = try await PluginDirectoryServiceRemote().getPluginInformation(slug: "rename-xml-rpc") + XCTAssertEqual(plugin.name, "Rename XMLRPC") + } + + func testGetDirectoryFeed() async throws { + let data = try MockPluginDirectoryProvider.getPluginDirectoryMockData(with: "plugin-directory-popular", sender: type(of: self)) + stub(condition: isHost("api.wordpress.org")) { _ in + HTTPStubsResponse(data: data, statusCode: 200, headers: ["Content-Type": "application/json"]) + } + + let feed = try await PluginDirectoryServiceRemote().getPluginFeed(.popular) + XCTAssertEqual(feed.plugins.first?.name, "Contact Form 7") + } + func testValidateResponseFound() { let data = try! MockPluginDirectoryProvider.getPluginDirectoryMockData(with: "plugin-directory-rename-xml-rpc", sender: type(of: self)) let endpoint = PluginDirectoryGetInformationEndpoint(slug: "jetpack") do { let request = try endpoint.buildRequest() let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)! - XCTAssertNoThrow(try endpoint.validate(request: request, response: response, data: data)) + XCTAssertNoThrow(try endpoint.validate(response: response, data: data)) } catch { XCTFail(error.localizedDescription) } @@ -70,7 +91,7 @@ class PluginDirectoryTests: XCTestCase { let request = try! endpoint.buildRequest() let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)! - XCTAssertThrowsError(try endpoint.validate(request: request, response: response, data: "null".data(using: .utf8))) + XCTAssertThrowsError(try endpoint.validate(response: response, data: "null".data(using: .utf8))) } func testValidatePluginDirectoryFeedResponseSucceeds() throws { @@ -79,7 +100,7 @@ class PluginDirectoryTests: XCTestCase { let request = try endpoint.buildRequest() let response = HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)! - XCTAssertNoThrow(try endpoint.validate(request: request, response: response, data: "null".data(using: .utf8))) + XCTAssertNoThrow(try endpoint.validate(response: response, data: "null".data(using: .utf8))) } func testValidatePluginDirectoryFeedResponseFails() { @@ -88,7 +109,7 @@ class PluginDirectoryTests: XCTestCase { let request = try! endpoint.buildRequest() let response = HTTPURLResponse(url: request.url!, statusCode: 403, httpVersion: "1.1", headerFields: nil)! - XCTAssertThrowsError(try endpoint.validate(request: request, response: response, data: "null".data(using: .utf8))) + XCTAssertThrowsError(try endpoint.validate(response: response, data: "null".data(using: .utf8))) } func testNewDirectoryFeedRequest() {