diff --git a/Podfile.lock b/Podfile.lock index 65bc9b7c..06b154ce 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -27,7 +27,7 @@ PODS: - OHHTTPStubs/Swift (6.1.0): - OHHTTPStubs/Default - UIDeviceIdentifier (1.1.4) - - WordPressKit (1.7.0-beta.4): + - WordPressKit (1.8.0-beta.1): - Alamofire (~> 4.7.3) - CocoaLumberjack (= 3.4.2) - NSObject-SafeExpectations (= 0.0.3) @@ -70,7 +70,7 @@ SPEC CHECKSUMS: OCMock: 43565190abc78977ad44a61c0d20d7f0784d35ab OHHTTPStubs: 1e21c7d2c084b8153fc53d48400d8919d2d432d0 UIDeviceIdentifier: 8f8a24b257a4d978c8d40ad1e7355b944ffbfa8c - WordPressKit: c46c1ec1e175282742e88a851d0066c77eed1cbd + WordPressKit: f2edbc8f99f7c698306193cfe216fd6e5b74fa54 WordPressShared: a2fc2db66c210a05d317ae9678b5823dd6a4d708 wpxmlrpc: 6ba55c773cfa27083ae4a2173e69b19f46da98e2 diff --git a/WordPressKit.podspec b/WordPressKit.podspec index 566daa8b..5f69fa5d 100644 --- a/WordPressKit.podspec +++ b/WordPressKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "WordPressKit" - s.version = "1.7.0" + s.version = "1.8.0-beta.1" s.summary = "WordPressKit offers a clean and simple WordPress.com and WordPress.org API." s.description = <<-DESC diff --git a/WordPressKit.xcodeproj/project.pbxproj b/WordPressKit.xcodeproj/project.pbxproj index b26c37eb..9b821d27 100644 --- a/WordPressKit.xcodeproj/project.pbxproj +++ b/WordPressKit.xcodeproj/project.pbxproj @@ -411,6 +411,11 @@ B5A4822B20AC6C0B009D95F6 /* CocoaLumberjack.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A4822A20AC6C0B009D95F6 /* CocoaLumberjack.swift */; }; B5A4822E20AC6C1A009D95F6 /* WPKitLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = B5A4822C20AC6C19009D95F6 /* WPKitLogging.m */; }; B5A4822F20AC6C1A009D95F6 /* WPKitLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = B5A4822D20AC6C1A009D95F6 /* WPKitLogging.h */; }; + D813437621F6D70D0060D99A /* SiteSegmentsResponseDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D813437521F6D70D0060D99A /* SiteSegmentsResponseDecodingTests.swift */; }; + D813437821F6D7DC0060D99A /* site-segments-single.json in Resources */ = {isa = PBXBuildFile; fileRef = D813437721F6D7DC0060D99A /* site-segments-single.json */; }; + D816857121EDACD10049883E /* WordPressComServiceRemote+SiteSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = D816857021EDACD10049883E /* WordPressComServiceRemote+SiteSegments.swift */; }; + D8DB404021EF222000B8238E /* SiteCreationSegmentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DB403F21EF222000B8238E /* SiteCreationSegmentsTests.swift */; }; + D8DB404221EF22B500B8238E /* site-segments-multiple.json in Resources */ = {isa = PBXBuildFile; fileRef = D8DB404121EF22B500B8238E /* site-segments-multiple.json */; }; E11C2AD21FA77FB90023BDE2 /* SitePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11C2AD11FA77FB90023BDE2 /* SitePlugin.swift */; }; E13EE1471F33258E00C15787 /* PluginServiceRemote.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13EE1461F33258E00C15787 /* PluginServiceRemote.swift */; }; E13EE1491F332B8500C15787 /* site-plugins-success.json in Resources */ = {isa = PBXBuildFile; fileRef = E13EE1481F332B8500C15787 /* site-plugins-success.json */; }; @@ -875,6 +880,11 @@ BEEC8B5D92DA614468900BD7 /* Pods-WordPressKit.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKit.release-alpha.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressKit/Pods-WordPressKit.release-alpha.xcconfig"; sourceTree = ""; }; C5953994B3865AF409BA4210 /* Pods-WordPressKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressKitTests/Pods-WordPressKitTests.release.xcconfig"; sourceTree = ""; }; CA5ABD95F40077D001644BCC /* Pods-WordPressKit.release-internal.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressKit.release-internal.xcconfig"; path = "Pods/Target Support Files/Pods-WordPressKit/Pods-WordPressKit.release-internal.xcconfig"; sourceTree = ""; }; + D813437521F6D70D0060D99A /* SiteSegmentsResponseDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteSegmentsResponseDecodingTests.swift; sourceTree = ""; }; + D813437721F6D7DC0060D99A /* site-segments-single.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-segments-single.json"; sourceTree = ""; }; + D816857021EDACD10049883E /* WordPressComServiceRemote+SiteSegments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WordPressComServiceRemote+SiteSegments.swift"; sourceTree = ""; }; + D8DB403F21EF222000B8238E /* SiteCreationSegmentsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteCreationSegmentsTests.swift; sourceTree = ""; }; + D8DB404121EF22B500B8238E /* site-segments-multiple.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "site-segments-multiple.json"; sourceTree = ""; }; E11C2AD11FA77FB90023BDE2 /* SitePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SitePlugin.swift; sourceTree = ""; }; E13EE1461F33258E00C15787 /* PluginServiceRemote.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PluginServiceRemote.swift; sourceTree = ""; }; E13EE1481F332B8500C15787 /* site-plugins-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-plugins-success.json"; sourceTree = ""; }; @@ -1296,6 +1306,7 @@ 93F50A351F226B9300B5BEBA /* WordPressComServiceRemote.h */, 93F50A361F226B9300B5BEBA /* WordPressComServiceRemote.m */, 7328420321CD786C00126755 /* WordPressComServiceRemote+SiteCreation.swift */, + D816857021EDACD10049883E /* WordPressComServiceRemote+SiteSegments.swift */, 730E869E21E44EFD00753E1A /* WordPressComServiceRemote+SiteVerticals.swift */, 73A2F38921E7F81E00388609 /* WordPressComServiceRemote+SiteVerticalsPrompt.swift */, 9368C7A11EC62F800092CE8E /* WPStatsServiceRemote.h */, @@ -1419,6 +1430,8 @@ 93BD273E1EE732CC002BB00B /* Accounts */ = { isa = PBXGroup; children = ( + D813437521F6D70D0060D99A /* SiteSegmentsResponseDecodingTests.swift */, + D8DB403F21EF222000B8238E /* SiteCreationSegmentsTests.swift */, 93BD27401EE73311002BB00B /* AccountServiceRemoteRESTTests.swift */, 7403A2E51EF06F7000DED7DC /* AccountSettingsRemoteTests.swift */, 93F50A391F226BB600B5BEBA /* WordPressComServiceRemoteRestTests.swift */, @@ -1546,6 +1559,7 @@ 74D67F101F15C2D70010C5ED /* site-users-update-role-success.json */, 74D67F111F15C2D70010C5ED /* site-users-update-role-unknown-site-failure.json */, 7434E1DD1F17C3C900C40DDB /* site-users-update-role-unknown-user-failure.json */, + D8DB404121EF22B500B8238E /* site-segments-multiple.json */, 73D592FA21E550D300E4CF84 /* site-verticals-empty.json */, 73D592F821E550D200E4CF84 /* site-verticals-multiple.json */, 73A2F38B21E7FC2A00388609 /* site-verticals-prompt.json */, @@ -1591,6 +1605,7 @@ 740B23DE1F17FB4200067A2A /* xmlrpc-wp-getpost-bad-xml-failure.xml */, 740B23DF1F17FB4200067A2A /* xmlrpc-wp-getpost-invalid-id-failure.xml */, 740B23E01F17FB4200067A2A /* xmlrpc-wp-getpost-success.xml */, + D813437721F6D7DC0060D99A /* site-segments-single.json */, ); path = "Mock Data"; sourceTree = ""; @@ -1905,6 +1920,7 @@ 74D67F1F1F15C3240010C5ED /* people-send-invitation-success.json in Resources */, FFE247B020C891E6002DF3A2 /* WordPressComAuthenticateWithIDToken2FANeededSuccess.json in Resources */, 436D563E2118E34D00CEAA33 /* supported-states-success.json in Resources */, + D813437821F6D7DC0060D99A /* site-segments-single.json in Resources */, 93BD27561EE73442002BB00B /* auth-send-login-email-invalid-secret-failure.json in Resources */, 436D564C211CCCB900CEAA33 /* domain-contact-information-response-success.json in Resources */, 93BD275A1EE73442002BB00B /* is-available-email-success.json in Resources */, @@ -1966,6 +1982,7 @@ 93AC8ECA1ED32FD000900F5A /* stats-v1.1-country-views-day.json in Resources */, 74C473BF1EF32B64009918F2 /* site-export-bad-json-failure.json in Resources */, 74D67F151F15C2D70010C5ED /* site-roles-success.json in Resources */, + D8DB404221EF22B500B8238E /* site-segments-multiple.json in Resources */, 740B23E11F17FB4200067A2A /* xmlrpc-metaweblog-editpost-bad-xml-failure.xml in Resources */, 93AC8ED51ED32FD000900F5A /* stats-v1.1-tags-categories-views-day.json in Resources */, 17BF9A7520C7E18200BF57D2 /* reader-site-search-success-no-icon.json in Resources */, @@ -2160,6 +2177,7 @@ 73A2F38A21E7F81E00388609 /* WordPressComServiceRemote+SiteVerticalsPrompt.swift in Sources */, 740B23BB1F17EC7300067A2A /* PostServiceRemoteXMLRPC.m in Sources */, 74B5F0E31EF82D2100B411E7 /* SiteServiceRemoteWordPressComREST.m in Sources */, + D816857121EDACD10049883E /* WordPressComServiceRemote+SiteSegments.swift in Sources */, 74E2295C1F1E77290085F7F2 /* KeyringConnectionExternalUser.swift in Sources */, E1BD95151FD5A2B800CD5CE3 /* PluginDirectoryServiceRemote.swift in Sources */, 7430C9D71F1933210051B8E6 /* RemoteReaderCrossPostMeta.swift in Sources */, @@ -2293,6 +2311,7 @@ 74FC6F3B1F191BB400112505 /* NotificationSyncServiceRemoteTests.swift in Sources */, 731BA83821DECD97000FDFCD /* SiteCreationResponseDecodingTests.swift in Sources */, 74FA25F71F1FDA200044BC54 /* MediaServiceRemoteRESTTests.swift in Sources */, + D8DB404021EF222000B8238E /* SiteCreationSegmentsTests.swift in Sources */, 7433BC051EFC4556002D9E92 /* PlanServiceRemoteTests.swift in Sources */, 740B23D61F17F7C100067A2A /* XMLRPCTestable.swift in Sources */, FFE247A720C891D1002DF3A2 /* WordPressComOAuthTests.swift in Sources */, @@ -2315,6 +2334,7 @@ 93AC8EE21ED32FD000900F5A /* WPStatsServiceRemoteTests.m in Sources */, E1787DB2200E5690004CB3AF /* TimeZoneServiceRemoteTests.swift in Sources */, 740B23D21F17F6BB00067A2A /* PostServiceRemoteRESTTests.m in Sources */, + D813437621F6D70D0060D99A /* SiteSegmentsResponseDecodingTests.swift in Sources */, 436D56382118DC4B00CEAA33 /* TransactionsServiceRemoteTests.swift in Sources */, 9F3E0BA82087355E009CB5BA /* RemoteReaderSiteInfoSubscriptionTests.swift in Sources */, 7430C9BE1F192C0F0051B8E6 /* ReaderTopicServiceRemoteTests.m in Sources */, diff --git a/WordPressKit/WordPressComServiceRemote+SiteSegments.swift b/WordPressKit/WordPressComServiceRemote+SiteSegments.swift new file mode 100644 index 00000000..f67282bd --- /dev/null +++ b/WordPressKit/WordPressComServiceRemote+SiteSegments.swift @@ -0,0 +1,142 @@ +/// Models a type of site. +public struct SiteSegment { + public let identifier: Int64 // we use a numeric ID for segments; see p9wMUP-bH-612-p2 for discussion + public let title: String + public let subtitle: String + public let icon: URL? + public let iconColor: String? + public let mobile: Bool + + public init(identifier: Int64, title: String, subtitle: String, icon: URL?, iconColor: String?, mobile: Bool) { + self.identifier = identifier + self.title = title + self.subtitle = subtitle + self.icon = icon + self.iconColor = iconColor + self.mobile = mobile + } +} + +extension SiteSegment { + public static let blogSegmentIdentifier = Int64(1) +} + +extension SiteSegment: Equatable { + public static func ==(lhs: SiteSegment, rhs: SiteSegment) -> Bool { + return lhs.identifier == rhs.identifier + } +} + +extension SiteSegment: Decodable { + enum CodingKeys: String, CodingKey { + case segmentId = "id" + case segmentTypeTitle = "segment_type_title" + case segmentTypeSubtitle = "segment_type_subtitle" + case iconURL = "icon_URL" + case iconColor = "icon_color" + case mobile = "mobile" + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + identifier = try values.decode(Int64.self, forKey: .segmentId) + title = try values.decode(String.self, forKey: .segmentTypeTitle) + subtitle = try values.decode(String.self, forKey: .segmentTypeSubtitle) + if let iconString = try values.decodeIfPresent(String.self, forKey: .iconURL) { + icon = URL(string: iconString) + } else { + icon = nil + } + + if let iconColorString = try values.decodeIfPresent(String.self, forKey: .iconColor) { + var cleanIconColorString = iconColorString + if iconColorString.hasPrefix("#") { + cleanIconColorString = String(iconColorString.dropFirst(1)) + } + + iconColor = cleanIconColorString + } else { + iconColor = nil + } + + mobile = try values.decode(Bool.self, forKey: .mobile) + + } +} + +// MARK: - WordPressComServiceRemote (Site Segments) + +/// Describes the errors that could arise when searching for site verticals. +/// +/// - requestEncodingFailure: unable to encode the request parameters. +/// - responseDecodingFailure: unable to decode the server response. +/// - serviceFailure: the service returned an unexpected error. +/// +public enum SiteSegmentsError: Error { + case requestEncodingFailure + case responseDecodingFailure + case serviceFailure +} + +/// Advises the caller of results related to requests for site segments. +/// +/// - success: the site segments request succeeded with the accompanying result. +/// - failure: the site segments request failed due to the accompanying error. +/// +public enum SiteSegmentsResult { + case success([SiteSegment]) + case failure(SiteSegmentsError) +} + +public typealias SiteSegmentsServiceCompletion = (SiteSegmentsResult) -> Void + +/// Site segments service, exclusive to WordPress.com. +/// +public extension WordPressComServiceRemote { + func retrieveSegments(completion: @escaping SiteSegmentsServiceCompletion) { + let endpoint = "segments" + let remotePath = path(forEndpoint: endpoint, withVersion: ._2_0) + + wordPressComRestApi.GET( + remotePath, + parameters: nil, + success: { [weak self] responseObject, httpResponse in + DDLogInfo("\(responseObject) | \(String(describing: httpResponse))") + + guard let self = self else { + return + } + + do { + let response = try self.decodeResponse(responseObject: responseObject) + let validContent = self.validSegments(response) + completion(.success(validContent)) + } catch { + DDLogError("Failed to decode \([SiteVertical].self) : \(error.localizedDescription)") + completion(.failure(SiteSegmentsError.responseDecodingFailure)) + } + }, + failure: { error, httpResponse in + DDLogError("\(error) | \(String(describing: httpResponse))") + completion(.failure(SiteSegmentsError.serviceFailure)) + }) + } +} + +// MARK: - Serialization support + +private extension WordPressComServiceRemote { + private func decodeResponse(responseObject: AnyObject) throws -> [SiteSegment] { + let decoder = JSONDecoder() + let data = try JSONSerialization.data(withJSONObject: responseObject, options: []) + let response = try decoder.decode([SiteSegment].self, from: data) + + return response + } + + private func validSegments(_ allSegments: [SiteSegment]) -> [SiteSegment] { + return allSegments.filter { + return $0.mobile == true + } + } +} diff --git a/WordPressKitTests/Mock Data/site-segments-multiple.json b/WordPressKitTests/Mock Data/site-segments-multiple.json new file mode 100644 index 00000000..ba898eed --- /dev/null +++ b/WordPressKitTests/Mock Data/site-segments-multiple.json @@ -0,0 +1,56 @@ +[ +{ + "icon_URL" : "https://s.wp.com/i/mobile_segmentation_icons/monochrome/ic_blogger.png", + "icon_color" : "#0087be", + "id" : 1, + "mobile" : true, + "segment_type_subtitle" : "Share and discuss ideas, updates, or creations.", + "segment_type_title" : "Blog", + "slug" : "blog" +}, +{ + "icon_URL" : "https://s.wp.com/i/mobile_segmentation_icons/monochrome/ic_business.png", + "icon_color" : "#f0b849", + "id" : 2, + "mobile" : true, + "segment_type_subtitle" : "Promote products and services.", + "segment_type_title" : "Business", + "slug" : "business" +}, +{ + "icon_URL" : "https://s.wp.com/i/mobile_segmentation_icons/monochrome/ic_professional.png", + "icon_color" : "#f0b849", + "id" : 3, + "mobile" : true, + "segment_type_subtitle" : "Showcase your portfolio, skills or work.", + "segment_type_title" : "Professional", + "slug" : "professional" +}, +{ + "icon_URL" : "https://s.wp.com/i/mobile_segmentation_icons/monochrome/ic_educator.png", + "icon_color" : "#d94f4f", + "id" : 4, + "mobile" : true, + "segment_type_subtitle" : "Share school projects and class info.", + "segment_type_title" : "Education", + "slug" : "education" +}, +{ + "icon_URL" : "", + "icon_color" : "", + "id" : 5, + "mobile" : false, + "segment_type_subtitle" : "Sell your collection of products online.", + "segment_type_title" : "Online store", + "slug" : "online-store" +}, +{ + "icon_URL" : "https://s.wp.com/i/mobile_segmentation_icons/monochrome/ic_blank_canvas.png", + "icon_color" : "#87a6bc", + "id" : 6, + "mobile" : true, + "segment_type_subtitle" : "Start with a blank site.", + "segment_type_title" : "Blank Canvas", + "slug" : "blank-canvas" +} +] diff --git a/WordPressKitTests/Mock Data/site-segments-single.json b/WordPressKitTests/Mock Data/site-segments-single.json new file mode 100644 index 00000000..fb494ef3 --- /dev/null +++ b/WordPressKitTests/Mock Data/site-segments-single.json @@ -0,0 +1,9 @@ +{ + "icon_URL" : "https://s.wp.com/i/mobile_segmentation_icons/monochrome/ic_blogger.png", + "icon_color" : "#0087be", + "id" : 1, + "mobile" : true, + "segment_type_subtitle" : "Share and discuss ideas, updates, or creations.", + "segment_type_title" : "Blog", + "slug" : "blog" +} diff --git a/WordPressKitTests/SiteCreationSegmentsTests.swift b/WordPressKitTests/SiteCreationSegmentsTests.swift new file mode 100644 index 00000000..0d2b9447 --- /dev/null +++ b/WordPressKitTests/SiteCreationSegmentsTests.swift @@ -0,0 +1,34 @@ +import XCTest +@testable import WordPressKit + +final class SiteCreationSegmentsTests: RemoteTestCase, RESTTestable { + + func testSiteSegmentsRequest_Succeeds() { + // Given + let endpoint = "segments" + let fileName = "site-segments-multiple.json" + stubRemoteResponse(endpoint, filename: fileName, contentType: .ApplicationJSON) + + let expectedSegmentsCount = 5 + + // When, Then + let segmentsExpectation = expectation(description: "Initiate site segments request") + let remote = WordPressComServiceRemote(wordPressComRestApi: getRestApi()) + remote.retrieveSegments(completion: { result in + segmentsExpectation.fulfill() + switch result { + case .success(let segments): + XCTAssertNotNil(segments) + + let mobileSegmentsCount = segments.count + XCTAssertEqual(mobileSegmentsCount, expectedSegmentsCount) + + case .failure(_): + XCTFail() + } + }) + + waitForExpectations(timeout: timeout) + } + +} diff --git a/WordPressKitTests/SiteSegmentsResponseDecodingTests.swift b/WordPressKitTests/SiteSegmentsResponseDecodingTests.swift new file mode 100644 index 00000000..9edfa818 --- /dev/null +++ b/WordPressKitTests/SiteSegmentsResponseDecodingTests.swift @@ -0,0 +1,54 @@ +import XCTest +@testable import WordPressKit + +final class SiteSegmentsResponseDecodingTests: XCTestCase { + private struct MockValues { + static let identifier: Int64 = 1 + static let mobile = true + static let title = "Blog" + static let subtitle = "Share and discuss ideas, updates, or creations." + static let iconURL = URL(string: "https://s.wp.com/i/mobile_segmentation_icons/monochrome/ic_blogger.png") + static let iconColor = "0087be" + } + + private var segment: SiteSegment? + + + override func setUp() { + super.setUp() + + let mockFileURL = Bundle(for: type(of:self)).url(forResource: "site-segments-single", withExtension: "json")! + + let json = try! Data(contentsOf: mockFileURL) + + segment = try! JSONDecoder().decode(SiteSegment.self, from: json) + } + + override func tearDown() { + super.tearDown() + } + + func testIdentifierIsNotMutated() { + XCTAssertEqual(segment?.identifier, MockValues.identifier) + } + + func testTitleIsNotMutated() { + XCTAssertEqual(segment?.title, MockValues.title) + } + + func testSubtitleIsNotMutated() { + XCTAssertEqual(segment?.subtitle, MockValues.subtitle) + } + + func testMobileIsNotMutated() { + XCTAssertEqual(segment?.mobile, MockValues.mobile) + } + + func testIconURLIsNotMutated() { + XCTAssertEqual(segment?.icon, MockValues.iconURL) + } + + func testIconColorIsNotMutated() { + XCTAssertEqual(segment?.iconColor, MockValues.iconColor) + } +}