diff --git a/Frameworks/Data/Attachment.swift b/Frameworks/Data/Attachment.swift index 9ce0543fb..73fdbd5fa 100644 --- a/Frameworks/Data/Attachment.swift +++ b/Frameworks/Data/Attachment.swift @@ -16,7 +16,7 @@ public struct Attachment: Equatable { public let sizeInBytes: Int? public let durationInSeconds: Int? - init(url: String, mimeType: String?, title: String?, sizeInBytes: Int?, durationInSeconds: Int?) { + public init(url: String, mimeType: String?, title: String?, sizeInBytes: Int?, durationInSeconds: Int?) { self.url = url self.mimeType = mimeType diff --git a/Frameworks/Database/AccountInfo.swift b/Frameworks/Database/AccountInfo.swift new file mode 100644 index 000000000..985ecfd81 --- /dev/null +++ b/Frameworks/Database/AccountInfo.swift @@ -0,0 +1,21 @@ +// +// AccountInfo.swift +// Database +// +// Created by Brent Simmons on 7/3/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSCore +import RSDatabase + +// 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 { + return nil + } + return propertyList(withData: rawAccountInfo) as? AccountInfo +} diff --git a/Frameworks/Database/Constants.swift b/Frameworks/Database/Constants.swift index f082b305a..f65d1580a 100644 --- a/Frameworks/Database/Constants.swift +++ b/Frameworks/Database/Constants.swift @@ -51,5 +51,10 @@ public struct DatabaseKey { // Tag static let tagName = "tagName" + + // Author + static let name = "name" + static let avatarURL = "avatarURL" + static let emailAddress = "emailAddress" } diff --git a/Frameworks/Database/CreateStatements.sql b/Frameworks/Database/CreateStatements.sql index bcbf5f23b..0ed7b64c8 100644 --- a/Frameworks/Database/CreateStatements.sql +++ b/Frameworks/Database/CreateStatements.sql @@ -9,7 +9,7 @@ CREATE TABLE if not EXISTS tags(tagName TEXT NOT NULL, articleID TEXT NOT NULL, CREATE TABLE if not EXISTS attachments(articleID TEXT NOT NULL, url TEXT NOT NULL, mimeType TEXT, title TEXT, sizeInBytes INTEGER, durationInSeconds INTEGER, PRIMARY KEY(articleID, url)); -CREATE INDEX if not EXISTS feedIndex on articles (feedID); +CREATE INDEX if not EXISTS articles_feedID_index on articles (feedID); CREATE INDEX if not EXISTS tags_tagName_index on tags(tagName COLLATE NOCASE); diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index b91ed4361..7d620542c 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -222,16 +222,16 @@ private extension Database { let oneArticleDictionary = oneDictionary.mutableCopy() as! NSMutableDictionary let articleID = oneArticleDictionary[DatabaseKey.articleID]! - oneArticleDictionary.removeObject(forKey: articleIDKey) + oneArticleDictionary.removeObject(forKey: DatabaseKey.articleID) - let _ = database.rs_updateRows(with: oneArticleDictionary as [NSObject: AnyObject], whereKey: articleIDKey, equalsValue: articleID, tableName: articlesTableName) + let _ = database.rs_updateRows(with: oneArticleDictionary as [NSObject: AnyObject], whereKey: DatabaseKey.articleID, equalsValue: articleID, tableName: DatabaseTableName.articles) } } if !newArticleDictionaries.isEmpty { for oneNewArticleDictionary in newArticleDictionaries { - let _ = database.rs_insertRow(with: oneNewArticleDictionary as [NSObject: AnyObject], insertType: RSDatabaseInsertOrReplace, tableName: articlesTableName) + let _ = database.rs_insertRow(with: oneNewArticleDictionary as [NSObject: AnyObject], insertType: RSDatabaseInsertOrReplace, tableName: DatabaseTableName.articles) } } } @@ -256,7 +256,7 @@ private extension Database { var d = [String: AnyObject]() for oneArticle in articles { - let oneArticleID = (oneArticle as AnyObject).value(forKey: articleIDKey) as! String + let oneArticleID = (oneArticle as AnyObject).value(forKey: DatabaseKey.articleID) as! String d[oneArticleID] = oneArticle as AnyObject } return d diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index c0e2e5cd8..cad383ba2 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ 844BEE411F0AB3AB004AB7CD /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE371F0AB3AA004AB7CD /* Database.framework */; }; 844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; }; 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580661F0AEBCD003CCFA1 /* Constants.swift */; }; - 845580721F0AEE49003CCFA1 /* PropertyListTransformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580711F0AEE49003CCFA1 /* PropertyListTransformer.swift */; }; + 845580721F0AEE49003CCFA1 /* AccountInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580711F0AEE49003CCFA1 /* AccountInfo.swift */; }; 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580751F0AF670003CCFA1 /* Article+Database.swift */; }; 845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580771F0AF678003CCFA1 /* Folder+Database.swift */; }; 8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; }; @@ -113,7 +113,7 @@ 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = ""; }; 844BEE471F0AB3AB004AB7CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 845580661F0AEBCD003CCFA1 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 845580711F0AEE49003CCFA1 /* PropertyListTransformer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PropertyListTransformer.swift; sourceTree = ""; }; + 845580711F0AEE49003CCFA1 /* AccountInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountInfo.swift; sourceTree = ""; }; 845580751F0AF670003CCFA1 /* Article+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Article+Database.swift"; path = "Extensions/Article+Database.swift"; sourceTree = ""; }; 845580771F0AF678003CCFA1 /* Folder+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Folder+Database.swift"; path = "Extensions/Folder+Database.swift"; sourceTree = ""; }; 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ArticleStatus+Database.swift"; path = "Extensions/ArticleStatus+Database.swift"; sourceTree = ""; }; @@ -206,7 +206,7 @@ 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */, 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */, 84BB4BA31F119D4A00858766 /* Author+Database.swift */, - 845580711F0AEE49003CCFA1 /* PropertyListTransformer.swift */, + 845580711F0AEE49003CCFA1 /* AccountInfo.swift */, ); name = Extensions; sourceTree = ""; @@ -462,7 +462,7 @@ 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */, 845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */, 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */, - 845580721F0AEE49003CCFA1 /* PropertyListTransformer.swift in Sources */, + 845580721F0AEE49003CCFA1 /* AccountInfo.swift in Sources */, 8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */, 84BB4BA41F119D4A00858766 /* Author+Database.swift in Sources */, 84BB4BA91F11A32800858766 /* TagsManager.swift in Sources */, diff --git a/Frameworks/Database/Extensions/Article+Database.swift b/Frameworks/Database/Extensions/Article+Database.swift index 66714f532..811613d32 100644 --- a/Frameworks/Database/Extensions/Article+Database.swift +++ b/Frameworks/Database/Extensions/Article+Database.swift @@ -35,9 +35,9 @@ extension Article { let authors = PropertyListTransformer.authorsWithRow(row) let tags = PropertyListTransformer.tagsWithRow(row) let attachments = PropertyListTransformer.attachmentsWithRow(row) - let accountInfo = PropertyListTransformer.accountInfoWithRow(row) + let accountInfo = accountInfoWithRow(row) - self.init(account: account, feedID: feed, 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] + self.init(account: account, feedID: feed, 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) } func databaseDictionary() -> NSDictionary { diff --git a/Frameworks/Database/Extensions/ArticleStatus+Database.swift b/Frameworks/Database/Extensions/ArticleStatus+Database.swift index 37b150cf3..0cc99260f 100644 --- a/Frameworks/Database/Extensions/ArticleStatus+Database.swift +++ b/Frameworks/Database/Extensions/ArticleStatus+Database.swift @@ -27,7 +27,7 @@ extension ArticleStatus { dateArrived = NSDate.distantPast } - let accountInfoPlist = PropertyListTransformer.accountInfoWithRow(row) + let accountInfoPlist = accountInfoWithRow(row) self.init(articleID: articleID!, read: read, starred: starred, userDeleted: userDeleted, dateArrived: dateArrived!, accountInfo: accountInfoPlist) } diff --git a/Frameworks/Database/Extensions/Attachment+Database.swift b/Frameworks/Database/Extensions/Attachment+Database.swift index 60575514a..c8dca6bf3 100644 --- a/Frameworks/Database/Extensions/Attachment+Database.swift +++ b/Frameworks/Database/Extensions/Attachment+Database.swift @@ -7,22 +7,24 @@ // import Foundation +import Data extension Attachment { - convenience init?(databaseDictionary d: [String: Any]) { + init?(databaseDictionary d: [String: Any]) { guard let url = d[DatabaseKey.url] as? String else { return nil } let mimeType = d[DatabaseKey.mimeType] as? String let title = d[DatabaseKey.title] as? String + let sizeInBytes = d[DatabaseKey.sizeInBytes] as? Int let durationInSeconds = d[DatabaseKey.durationInSeconds] as? Int - self.init(url: url, mimeType: mimeType, title: title, durationInSeconds: durationInSeconds) + self.init(url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds) } - class func attachments(with plist: [Any]) -> [Attachment]? { + static func attachments(with plist: [Any]) -> [Attachment]? { return plist.flatMap{ (oneDictionary) -> Attachment? in if let d = oneDictionary as? [String: Any] { diff --git a/Frameworks/Database/Extensions/Author+Database.swift b/Frameworks/Database/Extensions/Author+Database.swift index 0dafdde83..39a7cea2f 100644 --- a/Frameworks/Database/Extensions/Author+Database.swift +++ b/Frameworks/Database/Extensions/Author+Database.swift @@ -7,33 +7,18 @@ // import Foundation +import Data +import RSDatabase extension Author { - private static let - convenience init?(databaseDictionary d: [String: Any]) { - - guard let url = d[DatabaseKey.url] as? String else { - return nil - } - let mimeType = d[DatabaseKey.mimeType] as? String - let title = d[DatabaseKey.title] as? String - let durationInSeconds = d[DatabaseKey.durationInSeconds] as? Int - - self.init(url: url, mimeType: mimeType, title: title, durationInSeconds: durationInSeconds) - + init?(row: FMResultSet) { + let name = row.string(forColumn: DatabaseKey.name) + let url = row.string(forColumn: DatabaseKey.url) + let avatarURL = row.string(forColumn: DatabaseKey.avatarURL) + let emailAddress = row.string(forColumn: DatabaseKey.emailAddress) self.init(name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress) } - - class func attachments(with plist: [Any]) -> [Attachment]? { - - return plist.flatMap{ (oneDictionary) -> Attachment? in - if let d = oneDictionary as? [String: Any] { - return Attachment(databaseDictionary: d) - } - return nil - } - } } diff --git a/Frameworks/Database/PropertyListTransformer.swift b/Frameworks/Database/PropertyListTransformer.swift deleted file mode 100644 index bae6dfb62..000000000 --- a/Frameworks/Database/PropertyListTransformer.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// AccountInfo.swift -// Database -// -// Created by Brent Simmons on 7/3/17. -// Copyright © 2017 Ranchero Software. All rights reserved. -// - -import Foundation -import RSDatabase -import Data - -// This allows for serializing structures such as Author, Attachment, and AccountInfo -// without having to create separate tables and lookup tables. -// While there are good strong arguments for using separate tables, -// we decided that the relative simplicity this allows is worth it. - -struct PropertyListTransformer { - - static func accountInfoWithRow(_ row: FMResultSet) -> AccountInfo? { - - guard let rawAccountInfo = row.data(forColumn: DatabaseKey.accountInfo) else { - return nil - } - return propertyList(withData: rawAccountInfo) as? AccountInfo - } - - static func tagsWithRow(_ row: FMResultSet) -> [String]? { - - guard let d = row.data(forColumn: DatabaseKey.tags) else { - return nil - } - return propertyList(withData: d) as? [String] - } - - static func attachmentsWithRow(_ row: FMResultSet) -> [Attachment]? { - - guard let d = row.data(forColumn: DatabaseKey.attachments) else { - return nil - } - guard let plist = propertyList(withData: d) as? [Any] else { - return nil - } - return Attachment.attachments(with: plist) - } - - static func authorsWithRow(_ row: FMResultSet) -> [Author]? { - - guard let d = row.data(forColumn: DatabaseKey.authors) else { - return nil - } - guard let plist = propertyList(withData: d) as? [Any] else { - return nil - } - return Author.authors(with: plist) - } - - static func propertyListWithRow(_ row: FMResultSet, column: String) -> Any? { - - guard let rawData = row.data(forColumn: column) else { - return nil - } - return propertyList(withData: rawData) - } - - static func propertyList(withData data: Data) -> Any? { - - do { - return try PropertyListSerialization.propertyList(fromData: rawAccountInfo, options: [], format: nil) - } catch { - return nil - } - } - - static func data(withPropertyList plist: Any) -> Data? { - - do { - return try PropertyListSerialization.data(from: plist, format: .binary, options: []) - } - catch { - return nil - } - } -} diff --git a/Frameworks/Database/TagsManager.swift b/Frameworks/Database/TagsManager.swift index 005ae5789..445ab8980 100644 --- a/Frameworks/Database/TagsManager.swift +++ b/Frameworks/Database/TagsManager.swift @@ -8,15 +8,18 @@ import Foundation import RSDatabase +import Data // Tags — and the non-existence of tags — are cached, once fetched, for the lifetime of the run. // This uses some extra memory but cuts way down on the amount of database time spent // maintaining the tags table. +typealias TagNameSet = Set + final class TagsManager { - private var articleIDCache = [String: ]() // articleID: tag - private var articleIDsWithNoTags = Set() + private var articleIDCache = [String: TagNameSet]() // articleID: tags + private var articleIDsWithNoTags = TagNameSet() private let queue: RSDatabaseQueue @@ -53,8 +56,6 @@ final class TagsManager { } } -typealias TagNameSet = Set - private extension TagsManager { func cacheTagsForArticle(_ article: Article, tags: TagNameSet) { @@ -183,7 +184,7 @@ private extension TagsManager { } } - func fetchTagsForArticleIDs(_ articleIDs: Set, database: FMDatabase) -> TagsTable] { + func fetchTagsForArticleIDs(_ articleIDs: Set, database: FMDatabase) -> TagsTable { var tagSpecifiers = TagsTable() @@ -193,7 +194,7 @@ private extension TagsManager { while rs.next() { - guard let oneTagName = rs.string(forColumn: DatabaseKey.tagName), oneArticleID = rs.string(forColumn: DatabaseKey.articleID) else { + guard let oneTagName = rs.string(forColumn: DatabaseKey.tagName), let oneArticleID = rs.string(forColumn: DatabaseKey.articleID) else { continue } if tagSpecifiers[oneArticleID] == nil { diff --git a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj index 64cce8d27..50196193d 100755 --- a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj +++ b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj @@ -135,6 +135,7 @@ 84CFF56A1AC3D1B000CEA6C8 /* RSScaling.m in Sources */ = {isa = PBXBuildFile; fileRef = 84CFF5681AC3D1B000CEA6C8 /* RSScaling.m */; }; 84CFF56D1AC3D20A00CEA6C8 /* NSImage+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84CFF56B1AC3D20A00CEA6C8 /* NSImage+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84CFF56E1AC3D20A00CEA6C8 /* NSImage+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */; }; + 84F20F831F16BA6200D8E682 /* PropertyList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F821F16BA6200D8E682 /* PropertyList.swift */; }; 84FE9FC31C00453900081CE9 /* NSStoryboard+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84FE9FC11C00453900081CE9 /* NSStoryboard+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84FE9FC41C00453900081CE9 /* NSStoryboard+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84FE9FC21C00453900081CE9 /* NSStoryboard+RSCore.m */; }; 84FEB4AC1D19D7F4004727E5 /* Date+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FEB4AB1D19D7F4004727E5 /* Date+Extensions.swift */; }; @@ -234,6 +235,7 @@ 84CFF5681AC3D1B000CEA6C8 /* RSScaling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RSScaling.m; sourceTree = ""; }; 84CFF56B1AC3D20A00CEA6C8 /* NSImage+RSCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSImage+RSCore.h"; sourceTree = ""; }; 84CFF56C1AC3D20A00CEA6C8 /* NSImage+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSImage+RSCore.m"; sourceTree = ""; }; + 84F20F821F16BA6200D8E682 /* PropertyList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyList.swift; sourceTree = ""; }; 84FE9FC11C00453900081CE9 /* NSStoryboard+RSCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSStoryboard+RSCore.h"; sourceTree = ""; }; 84FE9FC21C00453900081CE9 /* NSStoryboard+RSCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSStoryboard+RSCore.m"; sourceTree = ""; }; 84FEB4AB1D19D7F4004727E5 /* Date+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Date+Extensions.swift"; sourceTree = ""; }; @@ -377,6 +379,7 @@ 84FEB4AB1D19D7F4004727E5 /* Date+Extensions.swift */, 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */, 8414CBA61C95F2EA00333C12 /* Set+Extensions.swift */, + 84F20F821F16BA6200D8E682 /* PropertyList.swift */, ); name = Foundation; path = RSCore; @@ -686,6 +689,7 @@ 8432B1861DACA0E90057D6DF /* NSResponder-Extensions.swift in Sources */, 849B08981BF7BCE30090CEE4 /* NSPasteboard+RSCore.m in Sources */, 842635571D7FA1C800196285 /* NSTableView+Extensions.swift in Sources */, + 84F20F831F16BA6200D8E682 /* PropertyList.swift in Sources */, 84CFF5611AC3D0CE00CEA6C8 /* RSBinaryCache.m in Sources */, 84CFF5301AC3CB1900CEA6C8 /* NSDate+RSCore.m in Sources */, 84CFF5281AC3C9A200CEA6C8 /* NSArray+RSCore.m in Sources */, diff --git a/Frameworks/RSCore/RSCore/PropertyList.swift b/Frameworks/RSCore/RSCore/PropertyList.swift new file mode 100644 index 000000000..2246b0e5b --- /dev/null +++ b/Frameworks/RSCore/RSCore/PropertyList.swift @@ -0,0 +1,32 @@ +// +// PropertyList.swift +// RSCore +// +// Created by Brent Simmons on 7/12/17. +// Copyright © 2017 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +// These functions eat errors. + +public func propertyList(withData data: Data) -> Any? { + + do { + return try PropertyListSerialization.propertyList(from: data, options: [], format: nil) + } catch { + return nil + } +} + +// Create a binary plist. + +public func data(withPropertyList plist: Any) -> Data? { + + do { + return try PropertyListSerialization.data(fromPropertyList: plist, format: .binary, options: 0) + } + catch { + return nil + } +}