From 7415131e8d0f5748938a9cb19776e4b4b64ad9e5 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 9 Sep 2017 18:46:58 -0700 Subject: [PATCH] Make ParsedFeed.items a Set. Fix some build errors in Database.framework. --- Frameworks/Data/AccountInfo.swift | 12 ++++-- Frameworks/Data/Feed.swift | 2 +- Frameworks/Database/ArticlesTable.swift | 34 ++++++----------- Frameworks/Database/Database.swift | 11 +++--- .../Extensions/Article+Database.swift | 4 +- .../Extensions/ParsedItem+Database.swift | 15 +------- Frameworks/Database/StatusesTable.swift | 36 +++++++++++++----- .../RSParser/Feeds/JSON/JSONFeedParser.swift | 6 +-- .../RSParser/Feeds/JSON/RSSInJSONParser.swift | 6 +-- Frameworks/RSParser/Feeds/ParsedFeed.swift | 4 +- .../Feeds/XML/RSParsedFeedTransformer.swift | 10 ++--- ToDo.ooutline | Bin 3258 -> 3453 bytes 12 files changed, 69 insertions(+), 71 deletions(-) diff --git a/Frameworks/Data/AccountInfo.swift b/Frameworks/Data/AccountInfo.swift index 85a295fd0..adb184698 100644 --- a/Frameworks/Data/AccountInfo.swift +++ b/Frameworks/Data/AccountInfo.swift @@ -8,18 +8,24 @@ import Foundation +// This is used by an Account that needs to store extra info. +// It’s stored as a binary plist in the database. + public struct AccountInfo: Equatable { - var dictionary: [String: AnyObject]? + var plist: [String: AnyObject]? + init(plist: [String: AnyObject]) { + + self.plist = plist + } + public static func ==(lhs: AccountInfo, rhs: AccountInfo) -> Bool { return true // TODO } } -// AccountInfo is a plist-compatible dictionary that’s stored as a binary plist in the database. - //func accountInfoWithRow(_ row: FMResultSet) -> AccountInfo? { // // guard let rawAccountInfo = row.data(forColumn: DatabaseKey.accountInfo) else { diff --git a/Frameworks/Data/Feed.swift b/Frameworks/Data/Feed.swift index f507830ff..f5dc1ee37 100644 --- a/Frameworks/Data/Feed.swift +++ b/Frameworks/Data/Feed.swift @@ -18,7 +18,7 @@ public final class Feed: DisplayNameProvider, Hashable { public var name: String? public var editedName: String? public var articles = Set
() - public var accountInfo: [String: Any]? //If account needs to store more data + public var accountInfo: AccountInfo? //If account needs to store more data public let hashValue: Int public var nameForDisplay: String { diff --git a/Frameworks/Database/ArticlesTable.swift b/Frameworks/Database/ArticlesTable.swift index 0531631b4..fd4dcbea9 100644 --- a/Frameworks/Database/ArticlesTable.swift +++ b/Frameworks/Database/ArticlesTable.swift @@ -31,8 +31,7 @@ final class ArticlesTable: DatabaseTable { self.name = name self.accountID = accountID self.queue = queue - - let statusesTable = StatusesTable(queue: queue) + self.statusesTable = StatusesTable(queue: queue) let authorsTable = AuthorsTable(name: DatabaseTableName.authors) self.authorsLookupTable = DatabaseLookupTable(name: DatabaseTableName.authorsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.authorID, relatedTable: authorsTable, relationshipName: RelationshipName.authors) @@ -96,17 +95,16 @@ final class ArticlesTable: DatabaseTable { let feedID = feed.feedID let parsedItemArticleIDs = Set(parsedFeed.items.map { $0.databaseIdentifierWithFeed(feed) }) - let parsedItemsDictionary = parsedFeed.itemsDictionary(with: feed) - statusesTable.ensureStatusesForArticleIDs(parsedItemArticleIDs) { // 1 + statusesTable.ensureStatusesForArticleIDs(parsedItemArticleIDs) { (statusesDictionary) in // 1 - let filteredParsedItems = self.filterParsedItems(parsedItemsDictionary) // 2 + let filteredParsedItems = self.filterParsedItems(Set(parsedFeed.items), statusesDictionary) // 2 if filteredParsedItems.isEmpty { completion(nil, nil) return } - queue.update{ (database) in + self.queue.update{ (database) in let fetchedArticles = self.fetchArticlesForFeedID(feedID, withLimits: false, database: database) //3 let fetchedArticlesDictionary = fetchedArticles.dictionary() @@ -196,10 +194,7 @@ private extension ArticlesTable { func articleWithRow(_ row: FMResultSet) -> Article? { - guard let account = account else { - return nil - } - guard let article = Article(row: row, account: account) else { + guard let article = Article(row: row, accountID: accountID) else { return nil } @@ -360,23 +355,18 @@ private extension ArticlesTable { return status.dateArrived < maximumArticleCutoffDate } - func filterParsedItems(_ parsedItems: [String: ParsedItem], _ statuses: [String: ArticleStatus]) -> [String: ParsedItem] { + func filterParsedItems(_ parsedItems: Set, _ statuses: [String: ArticleStatus]) -> Set { // Drop parsedItems that we can ignore. - var d = [String: ParsedItem]() - - for (articleID, parsedItem) in parsedItems { - + return Set(parsedItems.filter{ (parsedItem) -> Bool in + let articleID = parsedItem.articleID if let status = statuses[articleID] { - if statusIndicatesArticleIsIgnorable(status) { - continue - } + return !statusIndicatesArticleIsIgnorable(status) } - d[articleID] = parsedItem - } - - return d + assertionFailure("Expected a status for each parsedItem.") + return true + }) } } diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index 1e72285e8..2a555df60 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -15,23 +15,24 @@ import Data public typealias ArticleResultBlock = (Set
) -> Void public typealias UnreadCountTable = [String: Int] // feedID: unreadCount public typealias UnreadCountCompletionBlock = (UnreadCountTable) -> Void //feedID: unreadCount -typealias UpdateArticlesWithFeedCompletionBlock = (Set
, Set
) -> Void +public typealias UpdateArticlesWithFeedCompletionBlock = (Set
?, Set
?) -> Void //newArticles, updateArticles public final class Database { + private let accountID: String private let queue: RSDatabaseQueue private let databaseFile: String private let articlesTable: ArticlesTable private var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! private let minimumNumberOfArticles = 10 - public init(databaseFile: String) { + public init(databaseFile: String, accountID: String) { - self.account = account + self.accountID = accountID self.databaseFile = databaseFile self.queue = RSDatabaseQueue(filepath: databaseFile, excludeFromBackup: false) - self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, account: account, queue: queue) + self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, accountID: accountID, queue: queue) let createStatementsPath = Bundle(for: type(of: self)).path(forResource: "CreateStatements", ofType: "sql")! let createStatements = try! NSString(contentsOfFile: createStatementsPath, encoding: String.Encoding.utf8.rawValue) @@ -65,7 +66,7 @@ public final class Database { // MARK: - Updating Articles - public func update(feed: Feed, parsedFeed: ParsedFeed, completion: @escaping RSVoidCompletionBlock) { + public func update(feed: Feed, parsedFeed: ParsedFeed, completion: @escaping UpdateArticlesWithFeedCompletionBlock) { return articlesTable.update(feed, parsedFeed, completion) } diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index f25ce50c7..6db9acb10 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -33,7 +33,7 @@ extension Article { let bannerImageURL = row.string(forColumn: DatabaseKey.bannerImageURL) let datePublished = row.date(forColumn: DatabaseKey.datePublished) let dateModified = row.date(forColumn: DatabaseKey.dateModified) - let accountInfo: [String: Any]? = nil // TODO + let accountInfo: AccountInfo? = nil // TODO self.init(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo) } @@ -125,7 +125,7 @@ extension Article { return d } - static func articlesWithParsedItems(_ parsedItems: [ParsedItem], _ accountID: String, _ feedID: String) -> Set
{ + static func articlesWithParsedItems(_ parsedItems: Set, _ accountID: String, _ feedID: String) -> Set
{ return Set(parsedItems.map{ Article(parsedItem: $0, accountID: accountID, feedID: feedID) }) } diff --git a/Frameworks/Database/Extensions/ParsedItem+Database.swift b/Frameworks/Database/Extensions/ParsedItem+Database.swift index d02c98052..cf53c8455 100644 --- a/Frameworks/Database/Extensions/ParsedItem+Database.swift +++ b/Frameworks/Database/Extensions/ParsedItem+Database.swift @@ -23,19 +23,6 @@ extension ParsedItem { } } -extension ParsedFeed { - - func itemsDictionary(with feed: Feed) -> [String: ParsedItem] { - - var d = [String: ParsedItem]() - - for parsedItem in items { - let identifier = parsedItem.databaseIdentifierWithFeed(feed) - d[identifier] = parsedItem - } - - return d - } -} + diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index 64905361a..1f59dbffd 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -15,6 +15,8 @@ import Data // // CREATE TABLE if not EXISTS statuses (articleID TEXT NOT NULL PRIMARY KEY, read BOOL NOT NULL DEFAULT 0, starred BOOL NOT NULL DEFAULT 0, userDeleted BOOL NOT NULL DEFAULT 0, dateArrived DATE NOT NULL DEFAULT 0, accountInfo BLOB); +typealias StatusesCompletionBlock = ([String: ArticleStatus]) -> Void // [articleID: Status] + final class StatusesTable: DatabaseTable { let name = DatabaseTableName.statuses @@ -35,7 +37,7 @@ final class StatusesTable: DatabaseTable { // MARK: Creating/Updating - func ensureStatusesForArticleIDs(_ articleIDs: Set, _ completion: @escaping RSVoidCompletionBlock) { + func ensureStatusesForArticleIDs(_ articleIDs: Set, _ completion: @escaping StatusesCompletionBlock) { // Adds them to the cache if not cached. @@ -44,21 +46,21 @@ final class StatusesTable: DatabaseTable { // Check cache. let articleIDsMissingCachedStatus = articleIDsWithNoCachedStatus(articleIDs) if articleIDsMissingCachedStatus.isEmpty { - completion() + completion(statusesDictionary(articleIDs)) return } // Check database. fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus) { - let articleIDsNeedingStatus = articleIDsWithNoCachedStatus(articleIDs) + let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs) if articleIDsNeedingStatus.isEmpty { - completion() + completion(statusesDictionary(articleIDs)) return } // Create new statuses. - createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, completion) + self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, completion) } } @@ -95,16 +97,32 @@ private extension StatusesTable { func articleIDsWithNoCachedStatus(_ articleIDs: Set) -> Set { + assert(Thread.isMainThread) return Set(articleIDs.filter { cache[$0] == nil }) } + func statusesDictionary(_ articleIDs: Set) -> [String: ArticleStatus] { + + assert(Thread.isMainThread) + + var d = [String: ArticleStatus]() + + for articleID in articleIDs { + if let articleStatus = cache[articleID] { + d[articleID] = articleStatus + } + } + + return d + } + // MARK: Creating func saveStatuses(_ statuses: Set) { queue.update { (database) in let statusArray = statuses.map { $0.databaseDictionary() } - insertRows(statusArray, insertType: .orIgnore, in: database) + self.insertRows(statusArray, insertType: .orIgnore, in: database) } } @@ -126,15 +144,15 @@ private extension StatusesTable { func fetchAndCacheStatusesForArticleIDs(_ articleIDs: Set, _ completion: @escaping RSVoidCompletionBlock) { queue.fetch { (database) in - guard let resultSet = selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else { + guard let resultSet = self.selectRowsWhere(key: DatabaseKey.articleID, inValues: Array(articleIDs), in: database) else { completion() return } - let statuses = resultSet.mapToSet(statusWithRow) + let statuses = resultSet.mapToSet(self.statusWithRow) DispatchQueue.main.async { - cache.addIfNotCached(statuses) + self.cache.addIfNotCached(statuses) completion() } } diff --git a/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift b/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift index 0e33c3f2b..1b4a958aa 100644 --- a/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift +++ b/Frameworks/RSParser/Feeds/JSON/JSONFeedParser.swift @@ -81,11 +81,11 @@ private extension JSONFeedParser { return hubs.isEmpty ? nil : hubs } - static func parseItems(_ itemsArray: JSONArray, _ feedURL: String) -> [ParsedItem] { + static func parseItems(_ itemsArray: JSONArray, _ feedURL: String) -> Set { - return itemsArray.flatMap { (oneItemDictionary) -> ParsedItem? in + return Set(itemsArray.flatMap { (oneItemDictionary) -> ParsedItem? in return parseItem(oneItemDictionary, feedURL) - } + }) } static func parseItem(_ itemDictionary: JSONDictionary, _ feedURL: String) -> ParsedItem? { diff --git a/Frameworks/RSParser/Feeds/JSON/RSSInJSONParser.swift b/Frameworks/RSParser/Feeds/JSON/RSSInJSONParser.swift index 3d0a600ce..baa6a7a47 100644 --- a/Frameworks/RSParser/Feeds/JSON/RSSInJSONParser.swift +++ b/Frameworks/RSParser/Feeds/JSON/RSSInJSONParser.swift @@ -58,12 +58,12 @@ public struct RSSInJSONParser { private extension RSSInJSONParser { - static func parseItems(_ itemsObject: JSONArray, _ feedURL: String) -> [ParsedItem] { + static func parseItems(_ itemsObject: JSONArray, _ feedURL: String) -> Set { - return itemsObject.flatMap{ (oneItemDictionary) -> ParsedItem? in + return Set(itemsObject.flatMap{ (oneItemDictionary) -> ParsedItem? in return parsedItemWithDictionary(oneItemDictionary, feedURL) - } + }) } static func parsedItemWithDictionary(_ itemDictionary: JSONDictionary, _ feedURL: String) -> ParsedItem? { diff --git a/Frameworks/RSParser/Feeds/ParsedFeed.swift b/Frameworks/RSParser/Feeds/ParsedFeed.swift index 15a8686bb..2cad63fc6 100644 --- a/Frameworks/RSParser/Feeds/ParsedFeed.swift +++ b/Frameworks/RSParser/Feeds/ParsedFeed.swift @@ -21,9 +21,9 @@ public struct ParsedFeed { public let authors: [ParsedAuthor]? public let expired: Bool public let hubs: [ParsedHub]? - public let items: [ParsedItem] + public let items: Set - init(type: FeedType, title: String?, homePageURL: String?, feedURL: String?, feedDescription: String?, nextURL: String?, iconURL: String?, faviconURL: String?, authors: [ParsedAuthor]?, expired: Bool, hubs: [ParsedHub]?, items:[ParsedItem]) { + init(type: FeedType, title: String?, homePageURL: String?, feedURL: String?, feedDescription: String?, nextURL: String?, iconURL: String?, faviconURL: String?, authors: [ParsedAuthor]?, expired: Bool, hubs: [ParsedHub]?, items: Set) { self.type = type self.title = title diff --git a/Frameworks/RSParser/Feeds/XML/RSParsedFeedTransformer.swift b/Frameworks/RSParser/Feeds/XML/RSParsedFeedTransformer.swift index c5d68b40f..ced74ec5f 100644 --- a/Frameworks/RSParser/Feeds/XML/RSParsedFeedTransformer.swift +++ b/Frameworks/RSParser/Feeds/XML/RSParsedFeedTransformer.swift @@ -24,15 +24,11 @@ struct RSParsedFeedTransformer { private extension RSParsedFeedTransformer { - static func parsedItems(_ parsedArticles: Set) -> [ParsedItem] { + static func parsedItems(_ parsedArticles: Set) -> Set { - // Create [ParsedItem] from set of RSParsedArticle. + // Create Set from Set - var items = [ParsedItem]() - for oneParsedArticle in parsedArticles { - items += [parsedItem(oneParsedArticle)] - } - return items + return Set(parsedArticles.map(parsedItem)) } static func parsedItem(_ parsedArticle: RSParsedArticle) -> ParsedItem { diff --git a/ToDo.ooutline b/ToDo.ooutline index 13a6de0eff4d01e9038e67eaa71f5ffce4281ceb..a86c8c25af61e8fc709d540a810a3d53a27c6354 100644 GIT binary patch literal 3453 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 literal 3258 zcmZ{nRa6v?w#J7chLCRQ8d6I7AOZuZ49Xxi)Khz9+&61FW^yc4bt3~;i1!3SnKc+vW+Px@()ltu=1J~b45 z`*9qYSQQj*^Ld?5-2QxK5#g!*bIi|eCZQ<)DtrafWL`2z(@P{l-9WL?GT%Ot@@E&l z6K)d;)!KS06Os>^eLZrGs8`4w73%H7=J6g15xHQPzI;3P$HN$+ZiFl-y5kKl(KNmW zMWdvBDI(P&AWEARZwg7k+If_E%{q&Wdq7(6-4Qz}oA`i12n-W>Tl&@3qRPKpC>4%n z%z1#Lec3_^m37cq6|gRdj_>(15E*fU%T;s*B(>!5maNqh<^ZB~r0y~43Di-gT5*AA ziVf3T)drlqw7Js+c~-cQT+m##@iY;V+>2jcL(cwR^;e#_v0xYR+>7E}f)%C~=rctt zkfIu~$5-_zRmpJ3*3r(EiMwJ4=Z(;4FnH=>%t3w7VkIAOsIQY30Viht0>Rp-rl~{$ z`4xNB6U;@^IEH*`OSWAbE84ZQnAK87&FKZweRKXQZ2i&cq%u1iAI! zE-;Z%rSXajC-Jz^cnYychM2^rM$FDx6y4tmkJ92ZT^%Sc2<5dt zf^{5od~KUMt#jlHBuqKQ$* zUI4m^jhy>#-#q4`* z=J87H+K{WXConTe)miZo7!7`iE(&}5Ev$f!W<5WeFQEB*nKQfV1&q>(rdYSj!WBsdCZ}^Ec zJj0Jf1auIa+cGVk#&Hpp-l7%~R!nPPuuJknEO>ZV|0!Pdj)YvfL4M&5wVcju7uOZJ zk_l#Sr~f!ap?QjNX4kF~SvWmVYKCFm+B06`oX*~@V6S^MV`~TUsXgW5^!L|7%ltY= z9(u6_pR#_xv!%LUZe07(|J%fTbB6z?$n>*o73{r>aeHR#yg}$tN7a*&&yDGTyz{Cl z{4{qG{EGopj#<-&F0>IhKPhw8y{PpIepqH`(&*S$6Hs#u^E6d{5X=)dNPksb3r`se za$~p(D}Cc*uYXhNwD0x7)1S1Pr$*61y1<$5RdGjivxGnX`(9!-F%|?D|D~AaERO3pu5n&2yd)cYmCJ27i=eEc1dP& z=N*Qn4bWC2Yt`U|RVka7__X#^S2@9pm`WT>e3pPuSa*(Fh!_O*(I$9V?CAAv>fIFS zZ4iTDV61zo{TGrPV$Xyw#FV>fHA4!RyEkcT5Xi8$3NTCSdCj7B@1Q zFzmt?jf)@Y86aCHs*t2*ADk>;Xt%NswfosYuU_2xc65^4X*k}tpBnRqv0Ov70-B}4 zn7JWmw(GoiP3Lp}CE*YmYu`x#Z8iP7;_K@Q*dvE&{r z$aJsUk2%RS^zzdqIA=NV#cqlh7OP`appo5#S(?7$&&hA)()2G-B&pVR%}$FK=v;%y z7bs~GN6CwEb>92eeHm9R(rTbUF;-yhdOTFWIYz#iW#y1Zs!4nhUK3s`Z>ER1ohuC&y~H`&SEf3E&0O(m9_TWh2aMhi1p|Z`rbz z?EZJ57=#l^`1OllZ{IX`bQX?wdM>>#$i zFnaDs(?kFgNOVn=K^Z_>NVbEc>0dP7j^M}gen$izyFe^?B z)Z-j@D1mwnA>~U@@Wla%6)AznLXuV9E)*k6t+&p}Dnv^eaaR`wec5o*^s9Y)74ZkM zI@WZ;u)BRFeKbkh^DU5G$Mh`jd>a++lwB(?IZ6FHf^WYYs3Qq@JZ}mniLiL)%pVu> zGNI~;yF~lPF2Of+SjLv~E1P;I3n5z^BvQlO_`TzT5kFa=B&XNtugR+*x0A9*`^j{= z5kW0#Squf#xllQ^M_ut`JzZks7KE0)cgd*4dlw5ORZ_t;u8aQ3?0V^T_78J@mmgMK zJd}F1QF@n;exzp?tRzTSQ`!*bNr1E8qAY1+%QQV3iwkwPpo96I6CCj{N=pz;AeX@b zR!5(Y@WUc50xAX=25%l02Cge5vb?j4QQ0-YOIh@_T{n{Xtvhr|tC-$-R{hAuCm%@l z`_8N%G)d(zevO7@bNPnF^id`yM**3n&Zo0TI0OuFb%89pJ^PoqSfpk|BLm+xDimLK zuBFYb-ip7nH*2Q2_g0JPpl&rV*OZ3$;fd?={f4XBXLwQ`2LuxMsHf}icL;AUKOwx1 zcY)2j@1syljxgh+B(0*zB~!-f^KZz0uvo|VNh}EeMAe)EOi5@C?%F4Vgg?hc0i`1U zgu_nKbvX5U^xzsD%)Uo0IVl|(Tj;CDu>WV}Qb}q_9{~XXNkRaC?5~xxv3}$3&SaECA z{{H!bn`h*EevDQwA)G%Z*5$igFR_%Y>JD#z5>9@5HR(>JQiLa+`V-9VCo)?1;8^T~ zXzFn2V4YT(;1Q85^OOjF>O%^5y4Cz{;gYV2&deDnpsJ4*n~@qhhBo>}A&jY3(J=8L z2kJu~lYF*n|4@NP3z~t>WV^uft!jP#*nHr1H;{Y`nv*O;O6v4^PE>ivhLi|NK5soCWu>0jh%A@QB;#RNBItdH z2;MGCa-+{v=q=z81r>HaP5i1Y2HS+fQLk@k3F(>1m4PSJrr>agP(S_F?O5zgCK9*<;@=%iX)SdLSGE{L; z3^o(q?jf{@cp3j}C+M_eK9Rk~6GjioRGUo?fF5LgZCrEYzY)b`y{T@aUkn-%%U+Kg z-gMZ^YbNGsHEnU6&!&89M$rFi8r}T+;yr>fp=Y@7@KQ^g2(in(IizhkbAI-;(w?WY zMcj<7(NOkX!p)W?(_%>%#J&NvJx*D?BM<9*MEtVoZ1G?(!^4wtGWVxBl7wJwW`Y@;?dvKjrY>2=Z^d!k~mi|2%