@@ -4,20 +4,56 @@ import Foundation
44/// Encapsulates details of a single feed returned by the Reader feed search API
55/// (read/feed?q=query)
66///
7+ /// The API returns different structures depending on the site type:
8+ /// - WordPress.com sites: Data at root level (URL, title, blog_ID)
9+ /// - Jetpack sites: Data in meta.data.feed
10+ /// - External RSS feeds: Data in meta.data.feed, blog_ID is "0"
11+ ///
712public struct ReaderFeed : Decodable {
8- public let url : URL ?
9- public let title : String ?
10- public let feedDescription : String ?
11- public let feedID : String ?
12- public let blogID : String ?
13- public let blavatarURL : URL ?
13+ /// Feed ID from meta.data.feed
14+ public var feedID : String ? {
15+ feed? . feedID
16+ }
1417
15- private enum CodingKeys : String , CodingKey {
16- case url = " URL "
17- case title = " title "
18- case feedID = " feed_ID "
19- case blogID = " blog_ID "
20- case meta = " meta "
18+ /// Site/Feed URL with fallback: data.site → data.feed
19+ /// Prioritizes site URL over feed URL for canonical representation
20+ public var url : URL ? {
21+ site? . url ?? feed? . url
22+ }
23+
24+ /// Site/Feed title with fallback: data.site → data.feed
25+ /// Prioritizes site name over feed name
26+ public var title : String ? {
27+ site? . name ?? feed? . name
28+ }
29+
30+ /// Feed description with fallback: data.site → data.feed
31+ public var description : String ? {
32+ site? . description ?? feed? . description
33+ }
34+
35+ /// Blog ID with fallback: data.feed.blog_ID → data.site.ID
36+ /// Returns nil if "0" (external RSS feeds)
37+ public var blogID : String ? {
38+ let id = feed? . blogID ?? site? . id. map ( String . init)
39+ return ( id == " 0 " ) ? nil : id
40+ }
41+
42+ /// Site icon/avatar URL, prioritizing data.site.icon.img over data.feed.image
43+ public var iconURL : URL ? {
44+ site? . iconURL ?? feed? . imageURL
45+ }
46+
47+ // MARK: - Decodable
48+
49+ /// Feed data from meta.data.feed
50+ private var feed : FeedData ?
51+
52+ /// Site data from meta.data.site
53+ private var site : SiteData ?
54+
55+ private enum CodingKeys : CodingKey {
56+ case meta
2157 }
2258
2359 private enum MetaKeys : CodingKey {
@@ -30,61 +66,52 @@ public struct ReaderFeed: Decodable {
3066 }
3167
3268 public init ( from decoder: Decoder ) throws {
33- // We have to manually decode the feed from the JSON, for a couple of reasons:
34- // - Some feeds have no `icon` dictionary
35- // - Some feeds have no `data` dictionary
36- // - We want to decode whatever we can get, and not fail if neither of those exist
37- let rootContainer = try decoder. container ( keyedBy: CodingKeys . self)
38-
39- var feedURL = try ? rootContainer. decodeIfPresent ( URL . self, forKey: . url)
40- var title = try ? rootContainer. decodeIfPresent ( String . self, forKey: . title)
41- feedID = try ? rootContainer. decode ( String . self, forKey: . feedID)
42- blogID = try ? rootContainer. decode ( String . self, forKey: . blogID)
43-
44- var feedDescription : String ?
45- var blavatarURL : URL ?
46-
47- // Try to parse both site and feed data from meta.data
48- do {
49- let metaContainer = try rootContainer. nestedContainer ( keyedBy: MetaKeys . self, forKey: . meta)
50- let dataContainer = try metaContainer. nestedContainer ( keyedBy: DataKeys . self, forKey: . data)
51-
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- }
67- } catch {
69+ let root = try decoder. container ( keyedBy: CodingKeys . self)
70+ if let meta = try ? root. nestedContainer ( keyedBy: MetaKeys . self, forKey: . meta) ,
71+ let data = try ? meta. nestedContainer ( keyedBy: DataKeys . self, forKey: . data) {
72+ self . feed = try ? data. decode ( FeedData . self, forKey: . feed)
73+ self . site = try ? data. decode ( SiteData . self, forKey: . site)
6874 }
75+ }
76+ }
77+
78+ // MARK: - Feed Data
6979
70- self . url = feedURL
71- self . title = title
72- self . feedDescription = feedDescription
73- self . blavatarURL = blavatarURL
80+ /// Represents feed-specific data from meta.data.feed
81+ private struct FeedData : Decodable {
82+ let feedID : String ?
83+ let blogID : String ?
84+ let name : String ?
85+ let url : URL ?
86+ let description : String ?
87+ let imageURL : URL ?
88+
89+ private enum CodingKeys : String , CodingKey {
90+ case feedID = " feed_ID "
91+ case blogID = " blog_ID "
92+ case name = " name "
93+ case url = " URL "
94+ case description = " description "
95+ case imageURL = " image "
7496 }
7597}
7698
77- private struct SiteOrFeedData : Decodable {
78- var title : String ?
79- var description : String ?
80- var iconURL : URL ?
81- var url : URL ?
99+ // MARK: - Site Data
100+
101+ /// Represents site-specific data from meta.data.site
102+ private struct SiteData : Decodable {
103+ let id : Int ?
104+ let name : String ?
105+ let url : URL ?
106+ let description : String ?
107+ let iconURL : URL ?
82108
83- enum CodingKeys : String , CodingKey {
84- case description
85- case icon
109+ private enum CodingKeys : String , CodingKey {
110+ case id = " ID "
111+ case name = " name "
86112 case url = " URL "
87- case name
113+ case description = " description "
114+ case icon = " icon "
88115 }
89116
90117 private enum IconKeys : CodingKey {
@@ -94,19 +121,16 @@ private struct SiteOrFeedData: Decodable {
94121 init ( from decoder: Decoder ) throws {
95122 let container = try decoder. container ( keyedBy: CodingKeys . self)
96123
97- title = try ? container. decodeIfPresent ( String . self, forKey: . name )
98- description = try ? container. decodeIfPresent ( String . self, forKey: . description )
124+ id = try ? container. decodeIfPresent ( Int . self, forKey: . id )
125+ name = try ? container. decodeIfPresent ( String . self, forKey: . name )
99126 url = try ? container. decodeIfPresent ( URL . self, forKey: . url)
127+ description = try ? container. decodeIfPresent ( String . self, forKey: . description)
100128
101- // Try to decode the icon URL from the nested icon dictionary
129+ // Decode icon.img if icon dictionary exists
102130 if let iconContainer = try ? container. nestedContainer ( keyedBy: IconKeys . self, forKey: . icon) {
103131 iconURL = try ? iconContainer. decode ( URL . self, forKey: . img)
132+ } else {
133+ iconURL = nil
104134 }
105135 }
106136}
107-
108- extension ReaderFeed : CustomStringConvertible {
109- public var description : String {
110- return " <Feed | URL: \( String ( describing: url) ) , title: \( String ( describing: title) ) , feedID: \( String ( describing: feedID) ) , blogID: \( String ( describing: blogID) ) > "
111- }
112- }
0 commit comments