Skip to content

Commit d319ef4

Browse files
keanjkmassel
authored andcommitted
Fix an issue with Reader failing to parse some search results (#25022)
* Remove loading state title * More resilient Reader Search response parsing * Update release notes
1 parent 10b1e7e commit d319ef4

File tree

9 files changed

+225
-261
lines changed

9 files changed

+225
-261
lines changed

Modules/Sources/WordPressKit/ReaderFeed.swift

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import Foundation
55
/// (read/feed?q=query)
66
///
77
public struct ReaderFeed: Decodable {
8-
public let url: URL
9-
public let title: String
8+
public let url: URL?
9+
public let title: String?
1010
public let feedDescription: String?
1111
public let feedID: String?
1212
public let blogID: String?
@@ -26,15 +26,7 @@ public struct ReaderFeed: Decodable {
2626

2727
private enum DataKeys: CodingKey {
2828
case site
29-
}
30-
31-
private enum SiteKeys: CodingKey {
32-
case description
33-
case icon
34-
}
35-
36-
private enum IconKeys: CodingKey {
37-
case img
29+
case feed
3830
}
3931

4032
public init(from decoder: Decoder) throws {
@@ -44,32 +36,77 @@ public struct ReaderFeed: Decodable {
4436
// - We want to decode whatever we can get, and not fail if neither of those exist
4537
let rootContainer = try decoder.container(keyedBy: CodingKeys.self)
4638

47-
url = try rootContainer.decode(URL.self, forKey: .url)
48-
title = try rootContainer.decode(String.self, forKey: .title)
39+
var feedURL = try? rootContainer.decodeIfPresent(URL.self, forKey: .url)
40+
var title = try? rootContainer.decodeIfPresent(String.self, forKey: .title)
4941
feedID = try? rootContainer.decode(String.self, forKey: .feedID)
5042
blogID = try? rootContainer.decode(String.self, forKey: .blogID)
5143

5244
var feedDescription: String?
5345
var blavatarURL: URL?
5446

47+
// Try to parse both site and feed data from meta.data
5548
do {
5649
let metaContainer = try rootContainer.nestedContainer(keyedBy: MetaKeys.self, forKey: .meta)
5750
let dataContainer = try metaContainer.nestedContainer(keyedBy: DataKeys.self, forKey: .data)
58-
let siteContainer = try dataContainer.nestedContainer(keyedBy: SiteKeys.self, forKey: .site)
59-
feedDescription = try? siteContainer.decode(String.self, forKey: .description)
6051

61-
let iconContainer = try siteContainer.nestedContainer(keyedBy: IconKeys.self, forKey: .icon)
62-
blavatarURL = try? iconContainer.decode(URL.self, forKey: .img)
52+
let siteData = try? dataContainer.decode(SiteOrFeedData.self, forKey: .site)
53+
let feedData = try? dataContainer.decode(SiteOrFeedData.self, forKey: .feed)
54+
55+
// Use data from either source, preferring site data when both are available
56+
feedDescription = siteData?.description ?? feedData?.description
57+
blavatarURL = siteData?.iconURL ?? feedData?.iconURL
58+
59+
// Fixes CMM-1002: in some cases, the backend fails to embed certain fields
60+
// directly in the feed object
61+
if feedURL == nil {
62+
feedURL = siteData?.url ?? feedData?.url
63+
}
64+
if title == nil {
65+
title = siteData?.title ?? feedData?.title
66+
}
6367
} catch {
6468
}
6569

70+
self.url = feedURL
71+
self.title = title
6672
self.feedDescription = feedDescription
6773
self.blavatarURL = blavatarURL
6874
}
6975
}
7076

77+
private struct SiteOrFeedData: Decodable {
78+
var title: String?
79+
var description: String?
80+
var iconURL: URL?
81+
var url: URL?
82+
83+
enum CodingKeys: String, CodingKey {
84+
case description
85+
case icon
86+
case url = "URL"
87+
case name
88+
}
89+
90+
private enum IconKeys: CodingKey {
91+
case img
92+
}
93+
94+
init(from decoder: Decoder) throws {
95+
let container = try decoder.container(keyedBy: CodingKeys.self)
96+
97+
title = try? container.decodeIfPresent(String.self, forKey: .name)
98+
description = try? container.decodeIfPresent(String.self, forKey: .description)
99+
url = try? container.decodeIfPresent(URL.self, forKey: .url)
100+
101+
// Try to decode the icon URL from the nested icon dictionary
102+
if let iconContainer = try? container.nestedContainer(keyedBy: IconKeys.self, forKey: .icon) {
103+
iconURL = try? iconContainer.decode(URL.self, forKey: .img)
104+
}
105+
}
106+
}
107+
71108
extension ReaderFeed: CustomStringConvertible {
72109
public var description: String {
73-
return "<Feed | URL: \(url), title: \(title), feedID: \(String(describing: feedID)), blogID: \(String(describing: blogID))>"
110+
return "<Feed | URL: \(String(describing: url)), title: \(String(describing: title)), feedID: \(String(describing: feedID)), blogID: \(String(describing: blogID))>"
74111
}
75112
}

Modules/Sources/WordPressKit/ReaderSiteSearchServiceRemote.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
2020
public func performSearch(_ query: String,
2121
offset: Int = 0,
2222
count: Int,
23-
success: @escaping (_ results: [ReaderFeed], _ hasMore: Bool, _ feedCount: Int) -> Void,
23+
success: @escaping (_ results: [ReaderFeed], _ hasMore: Bool, _ total: Int?) -> Void,
2424
failure: @escaping (Error) -> Void) {
2525
let endpoint = "read/feed"
2626
let path = self.path(forEndpoint: endpoint, withVersion: ._1_1)
@@ -29,7 +29,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
2929
"offset": offset as AnyObject,
3030
"exclude_followed": false as AnyObject,
3131
"sort": "relevance" as AnyObject,
32-
"meta": "site" as AnyObject,
32+
"meta": "site,feed" as AnyObject,
3333
"q": query as AnyObject
3434
]
3535

@@ -38,7 +38,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
3838
success: { response, _ in
3939
do {
4040
let (results, total) = try self.mapSearchResponse(response)
41-
let hasMore = total > (offset + count)
41+
let hasMore = (total ?? 0) > (offset + count)
4242
success(results, hasMore, total)
4343
} catch {
4444
failure(error)
@@ -52,7 +52,7 @@ public class ReaderSiteSearchServiceRemote: ServiceRemoteWordPressComREST {
5252

5353
private extension ReaderSiteSearchServiceRemote {
5454

55-
func mapSearchResponse(_ response: Any) throws -> ([ReaderFeed], Int) {
55+
func mapSearchResponse(_ response: Any) throws -> ([ReaderFeed], Int?) {
5656
do {
5757
let decoder = JSONDecoder()
5858
let data = try JSONSerialization.data(withJSONObject: response, options: [])
@@ -73,9 +73,9 @@ private extension ReaderSiteSearchServiceRemote {
7373
/// The Reader feed search endpoint returns feeds in a key named `feeds` key.
7474
/// This entity allows us to do parse that and the total feed count using JSONDecoder.
7575
///
76-
private struct ReaderFeedEnvelope: Decodable {
76+
struct ReaderFeedEnvelope: Decodable {
7777
let feeds: [ReaderFeed]
78-
let total: Int
78+
let total: Int?
7979

8080
private enum CodingKeys: String, CodingKey {
8181
case feeds = "feeds"

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* [*] Add "Email to Subscribers" row to "Publishing" sheet [#24946]
1515
* [*] Add permalink preview in the slug editor and make other improvements [#24949]
1616
* [*] Add two accessible font sizes to Reader display settings [#25013]
17+
* [*] Fix an issue with Reader failing to parse some search results [#25022]
1718
* [*] Add "Taxonomies" to Site Settings [#24955]
1819
* [*] Update "Categories" picker to indicate multiple selection [#24952]
1920
* [*] Fix a bug where the app can't access some Jetpack connected sites [#24976]

Tests/WordPressKitTests/WordPressKitTests/Mock Data/reader-site-search-failure.json

Lines changed: 0 additions & 192 deletions
This file was deleted.

Tests/WordPressKitTests/WordPressKitTests/Tests/ReaderSiteSearchServiceRemoteTests.swift

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ class ReaderSiteSearchServiceRemoteTests: RemoteTestCase, RESTTestable {
1010
let performSearchSuccessNoIconFilename = "reader-site-search-success-no-icon.json"
1111
let performSearchSuccessNoDataFilename = "reader-site-search-success-no-data.json"
1212
let performSearchSuccessHasMoreFilename = "reader-site-search-success-hasmore.json"
13-
let performSearchFailureFilename = "reader-site-search-failure.json"
1413
let performSearchBlogIDFallbackFilename = "reader-site-search-blog-id-fallback.json"
1514
let performSearchFailsWithNoBlogOrFeedIDFilename = "reader-site-search-no-blog-or-feed-id.json"
1615

@@ -148,27 +147,6 @@ class ReaderSiteSearchServiceRemoteTests: RemoteTestCase, RESTTestable {
148147
waitForExpectations(timeout: timeout, handler: nil)
149148
}
150149

151-
func testPerformSearchFailure() {
152-
let expect = expectation(description: "Perform Reader site search fails if no URL is present")
153-
154-
stubRemoteResponse(performSearchEndpoint, filename: performSearchFailureFilename, contentType: .ApplicationJSON)
155-
remote.performSearch("discover",
156-
count: 10, success: { (_, _, _) in
157-
XCTFail("This callback shouldn't get called")
158-
expect.fulfill()
159-
}, failure: { error in
160-
typealias ResponseError = ReaderSiteSearchServiceRemote.ResponseError
161-
guard case ResponseError.decodingFailure? = error as? ResponseError else {
162-
XCTFail("Expected a decodingFailure error")
163-
expect.fulfill()
164-
return
165-
}
166-
167-
expect.fulfill()
168-
})
169-
waitForExpectations(timeout: timeout, handler: nil)
170-
}
171-
172150
func testPerformSearchBlogIDFallback() {
173151
let expect = expectation(description: "Perform Reader site search falls back to parsing blog ID if no feed ID is present")
174152

0 commit comments

Comments
 (0)