From 15a0ba89d7fca0b665fbbda6106ac2f70e6b9840 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 5 May 2019 15:41:20 -0500 Subject: [PATCH] Add folder syncing for Feedbin account --- Frameworks/Account/Account.swift | 5 +- .../Account/Account.xcodeproj/project.pbxproj | 46 ++++++--- Frameworks/Account/AccountDelegate.swift | 2 +- Frameworks/Account/AccountMetadata.swift | 2 + .../AccountTests/AccountCredentialsTest.swift | 2 +- .../AccountTests/AccountFolderSyncTest.swift | 83 +++++++++++++++++ .../Account/AccountTests/AccountTests.swift | 36 ------- .../Account/AccountTests/JSON/tags_add.json | 42 +++++++++ .../AccountTests/JSON/tags_delete.json | 34 +++++++ .../AccountTests/JSON/tags_initial.json | 38 ++++++++ .../Account/AccountTests/NilTransport.swift | 21 ----- .../Account/AccountTests/TestTransport.swift | 34 +++++++ .../Account/Feedbin/FeedbinAPICaller.swift | 41 ++++++++ .../Feedbin/FeedbinAccountDelegate.swift | 93 ++++++++++++++++++- Frameworks/Account/Feedbin/FeedbinTag.swift | 21 +++++ .../Account/Feedbin/FeedbinTagging.swift | 62 +------------ .../LocalAccount/LocalAccountDelegate.swift | 5 +- .../AccountsFeedbinWindowController.swift | 5 + 18 files changed, 435 insertions(+), 137 deletions(-) create mode 100644 Frameworks/Account/AccountTests/AccountFolderSyncTest.swift delete mode 100644 Frameworks/Account/AccountTests/AccountTests.swift create mode 100644 Frameworks/Account/AccountTests/JSON/tags_add.json create mode 100644 Frameworks/Account/AccountTests/JSON/tags_delete.json create mode 100644 Frameworks/Account/AccountTests/JSON/tags_initial.json delete mode 100644 Frameworks/Account/AccountTests/NilTransport.swift create mode 100644 Frameworks/Account/AccountTests/TestTransport.swift create mode 100644 Frameworks/Account/Feedbin/FeedbinTag.swift diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index dbb908e60..3801fd064 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -281,9 +281,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } - public func refreshAll() { - - delegate.refreshAll(for: self) + public func refreshAll(completionHandler completion: (() -> Void)? = nil) { + delegate.refreshAll(for: self, completionHandler: completion) } public func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping RSVoidCompletionBlock) { diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index a405748e6..402342af1 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -9,9 +9,14 @@ /* Begin PBXBuildFile section */ 5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */; }; 5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; }; - 5107A09D227DE77700C7C3C5 /* NilTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* NilTransport.swift */; }; + 5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; }; 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; }; 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; }; + 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D58754227F53BE00900287 /* FeedbinTag.swift */; }; + 51D5875A227F630B00900287 /* tags_delete.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58757227F630B00900287 /* tags_delete.json */; }; + 51D5875B227F630B00900287 /* tags_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58758227F630B00900287 /* tags_add.json */; }; + 51D5875C227F630B00900287 /* tags_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 51D58759227F630B00900287 /* tags_initial.json */; }; + 51D5875E227F643C00900287 /* AccountFolderSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */; }; 841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973EF1F6DD19E006346C4 /* RSCore.framework */; }; 841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973FA1F6DD1AC006346C4 /* RSParser.framework */; }; 841974011F6DD1EC006346C4 /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841974001F6DD1EC006346C4 /* Folder.swift */; }; @@ -28,7 +33,6 @@ 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8419742D1F6DDE96006346C4 /* LocalAccountRefresher.swift */; }; 846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 846E77531F6F00E300A165E2 /* AccountManager.swift */; }; 848935001F62484F00CEBD24 /* Account.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 848934F61F62484F00CEBD24 /* Account.framework */; }; - 848935051F62485000CEBD24 /* AccountTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848935041F62485000CEBD24 /* AccountTests.swift */; }; 84B2D4D02238CD8A00498ADA /* FeedMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B2D4CE2238C13D00498ADA /* FeedMetadata.swift */; }; 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C9E1FAE8D3200ECDEDB /* ContainerPath.swift */; }; 84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C365491F899F3B001EC85C /* CombinedRefreshProgress.swift */; }; @@ -90,9 +94,14 @@ /* Begin PBXFileReference section */ 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsTest.swift; sourceTree = ""; }; 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = ""; }; - 5107A09C227DE77700C7C3C5 /* NilTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NilTransport.swift; sourceTree = ""; }; + 5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = ""; }; 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = ""; }; 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = ""; }; + 51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = ""; }; + 51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = ""; }; + 51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = ""; }; + 51D58759227F630B00900287 /* tags_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_initial.json; sourceTree = ""; }; + 51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderSyncTest.swift; sourceTree = ""; }; 841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = ""; }; 841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; 841974001F6DD1EC006346C4 /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = ""; }; @@ -110,7 +119,6 @@ 848934F61F62484F00CEBD24 /* Account.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Account.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 848934FA1F62484F00CEBD24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 848934FF1F62484F00CEBD24 /* AccountTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AccountTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 848935041F62485000CEBD24 /* AccountTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountTests.swift; sourceTree = ""; }; 848935061F62485000CEBD24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 848935101F62486800CEBD24 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; 84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadata.swift; sourceTree = ""; }; @@ -156,6 +164,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 51D58756227F62E300900287 /* JSON */ = { + isa = PBXGroup; + children = ( + 51D58758227F630B00900287 /* tags_add.json */, + 51D58757227F630B00900287 /* tags_delete.json */, + 51D58759227F630B00900287 /* tags_initial.json */, + ); + path = JSON; + sourceTree = ""; + }; 841973E91F6DD19E006346C4 /* Products */ = { isa = PBXGroup; children = ( @@ -189,11 +207,12 @@ children = ( 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */, 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */, - 84245C841FDDD8CB0074AFBB /* FeedbinFeed.swift */, 84CAD7151FDF2E22000F0755 /* FeedbinArticle.swift */, 84D096202174169100D77525 /* FeedbinArticleIDArray.swift */, - 84D09622217418DC00D77525 /* FeedbinTagging.swift */, + 84245C841FDDD8CB0074AFBB /* FeedbinFeed.swift */, 84D0962421741B8500D77525 /* FeedbinSavedSearch.swift */, + 51D58754227F53BE00900287 /* FeedbinTag.swift */, + 84D09622217418DC00D77525 /* FeedbinTagging.swift */, ); path = Feedbin; sourceTree = ""; @@ -250,11 +269,12 @@ 848935031F62484F00CEBD24 /* AccountTests */ = { isa = PBXGroup; children = ( - 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */, - 848935041F62485000CEBD24 /* AccountTests.swift */, 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */, + 51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */, + 5107A09C227DE77700C7C3C5 /* TestTransport.swift */, + 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */, + 51D58756227F62E300900287 /* JSON */, 848935061F62485000CEBD24 /* Info.plist */, - 5107A09C227DE77700C7C3C5 /* NilTransport.swift */, ); path = AccountTests; sourceTree = ""; @@ -419,6 +439,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 51D5875B227F630B00900287 /* tags_add.json in Resources */, + 51D5875C227F630B00900287 /* tags_initial.json in Resources */, + 51D5875A227F630B00900287 /* tags_delete.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -443,6 +466,7 @@ 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */, 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */, 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */, + 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */, 84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */, 84CAD7161FDF2E22000F0755 /* FeedbinArticle.swift in Sources */, 84D0962521741B8500D77525 /* FeedbinSavedSearch.swift in Sources */, @@ -458,10 +482,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 51D5875E227F643C00900287 /* AccountFolderSyncTest.swift in Sources */, 5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */, - 5107A09D227DE77700C7C3C5 /* NilTransport.swift in Sources */, + 5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */, 5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */, - 848935051F62485000CEBD24 /* AccountTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index 7afbf8ec3..505e2154f 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -19,7 +19,7 @@ protocol AccountDelegate { var refreshProgress: DownloadProgress { get } - func refreshAll(for: Account) + func refreshAll(for: Account, completionHandler completion: (() -> Void)?) // Called at the end of account’s init method. diff --git a/Frameworks/Account/AccountMetadata.swift b/Frameworks/Account/AccountMetadata.swift index 5a565375a..963dcd31c 100644 --- a/Frameworks/Account/AccountMetadata.swift +++ b/Frameworks/Account/AccountMetadata.swift @@ -17,6 +17,8 @@ final class AccountMetadata: Codable { struct ConditionalGetKeys { static let subscriptions = "subscriptions" + static let tags = "tags" + static let taggings = "taggings" } enum CodingKeys: String, CodingKey { diff --git a/Frameworks/Account/AccountTests/AccountCredentialsTest.swift b/Frameworks/Account/AccountTests/AccountCredentialsTest.swift index feda37dcd..d9e7ba91f 100644 --- a/Frameworks/Account/AccountTests/AccountCredentialsTest.swift +++ b/Frameworks/Account/AccountTests/AccountCredentialsTest.swift @@ -15,7 +15,7 @@ class AccountCredentialsTest: XCTestCase { private var account: Account! override func setUp() { - account = TestAccountManager.shared.createAccount(type: .feedbin, transport: NilTransport()) + account = TestAccountManager.shared.createAccount(type: .feedbin, transport: TestTransport()) } override func tearDown() { diff --git a/Frameworks/Account/AccountTests/AccountFolderSyncTest.swift b/Frameworks/Account/AccountTests/AccountFolderSyncTest.swift new file mode 100644 index 000000000..c81c76324 --- /dev/null +++ b/Frameworks/Account/AccountTests/AccountFolderSyncTest.swift @@ -0,0 +1,83 @@ +// +// AccountFolderSyncTest.swift +// AccountTests +// +// Created by Maurice Parker on 5/5/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import XCTest +@testable import Account + +class AccountFolderSyncTest: XCTestCase { + + override func setUp() { + } + + override func tearDown() { + } + + func testFolderSync() { + + let testTransport = TestTransport() + testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_initial.json" + let account = TestAccountManager.shared.createAccount(type: .feedbin, transport: testTransport) + + // Test initial folders + let initialExpection = self.expectation(description: "Initial tags") + account.refreshAll() { + initialExpection.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + + guard let intialFolders = account.folders else { + XCTFail() + return + } + + XCTAssertEqual(9, intialFolders.count) + let initialFolderNames = intialFolders.map { $0.name ?? "" } + XCTAssertTrue(initialFolderNames.contains("Outdoors")) + + // Test removing folders + testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_delete.json" + + let deleteExpection = self.expectation(description: "Delete tags") + account.refreshAll() { + deleteExpection.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + + guard let deleteFolders = account.folders else { + XCTFail() + return + } + + XCTAssertEqual(8, deleteFolders.count) + let deleteFolderNames = deleteFolders.map { $0.name ?? "" } + XCTAssertTrue(deleteFolderNames.contains("Outdoors")) + XCTAssertFalse(deleteFolderNames.contains("Tech Media")) + + // Test Adding Folders + testTransport.testFiles["https://api.feedbin.com/v2/tags.json"] = "tags_add.json" + + let addExpection = self.expectation(description: "Add tags") + account.refreshAll() { + addExpection.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + + guard let addFolders = account.folders else { + XCTFail() + return + } + + XCTAssertEqual(10, addFolders.count) + let addFolderNames = addFolders.map { $0.name ?? "" } + XCTAssertTrue(addFolderNames.contains("Vanlife")) + + TestAccountManager.shared.deleteAccount(account) + + } + +} diff --git a/Frameworks/Account/AccountTests/AccountTests.swift b/Frameworks/Account/AccountTests/AccountTests.swift deleted file mode 100644 index ee51f4a6c..000000000 --- a/Frameworks/Account/AccountTests/AccountTests.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AccountTests.swift -// AccountTests -// -// Created by Brent Simmons on 9/7/17. -// Copyright © 2017 Ranchero Software, LLC. All rights reserved. -// - -import XCTest -@testable import Account - -class AccountTests: XCTestCase { - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } - } - -} diff --git a/Frameworks/Account/AccountTests/JSON/tags_add.json b/Frameworks/Account/AccountTests/JSON/tags_add.json new file mode 100644 index 000000000..560b66e92 --- /dev/null +++ b/Frameworks/Account/AccountTests/JSON/tags_add.json @@ -0,0 +1,42 @@ +[ + { + "id": 9754, + "name": "Amusement" + }, + { + "id": 247, + "name": "Business" + }, + { + "id": 1049, + "name": "Developers" + }, + { + "id": 102244, + "name": "Development Orgs" + }, + { + "id": 40541, + "name": "Open Web" + }, + { + "id": 1337, + "name": "Outdoors" + }, + { + "id": 56975, + "name": "Overlanding" + }, + { + "id": 3782, + "name": "Pundits" + }, + { + "id": 7827, + "name": "Tech Media" + }, + { + "id": 97769, + "name": "Vanlife" + } +] \ No newline at end of file diff --git a/Frameworks/Account/AccountTests/JSON/tags_delete.json b/Frameworks/Account/AccountTests/JSON/tags_delete.json new file mode 100644 index 000000000..39daf23e1 --- /dev/null +++ b/Frameworks/Account/AccountTests/JSON/tags_delete.json @@ -0,0 +1,34 @@ +[ + { + "id": 9754, + "name": "Amusement" + }, + { + "id": 247, + "name": "Business" + }, + { + "id": 1049, + "name": "Developers" + }, + { + "id": 102244, + "name": "Development Orgs" + }, + { + "id": 40541, + "name": "Open Web" + }, + { + "id": 1337, + "name": "Outdoors" + }, + { + "id": 56975, + "name": "Overlanding" + }, + { + "id": 3782, + "name": "Pundits" + } +] diff --git a/Frameworks/Account/AccountTests/JSON/tags_initial.json b/Frameworks/Account/AccountTests/JSON/tags_initial.json new file mode 100644 index 000000000..d3f1f6477 --- /dev/null +++ b/Frameworks/Account/AccountTests/JSON/tags_initial.json @@ -0,0 +1,38 @@ +[ + { + "id": 9754, + "name": "Amusement" + }, + { + "id": 247, + "name": "Business" + }, + { + "id": 1049, + "name": "Developers" + }, + { + "id": 102244, + "name": "Development Orgs" + }, + { + "id": 40541, + "name": "Open Web" + }, + { + "id": 1337, + "name": "Outdoors" + }, + { + "id": 56975, + "name": "Overlanding" + }, + { + "id": 3782, + "name": "Pundits" + }, + { + "id": 7827, + "name": "Tech Media" + } +] diff --git a/Frameworks/Account/AccountTests/NilTransport.swift b/Frameworks/Account/AccountTests/NilTransport.swift deleted file mode 100644 index d8c2566f5..000000000 --- a/Frameworks/Account/AccountTests/NilTransport.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// NilTransport.swift -// AccountTests -// -// Created by Maurice Parker on 5/4/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import RSWeb - -struct NilTransport: Transport { - - func send(request: URLRequest, resultType: T.Type, completion: @escaping (Result<(HTTPHeaders, T), Error>) -> Void) where T : Decodable, T : Encodable { - } - - - func send(request: URLRequest, completion: @escaping (Result<(HTTPHeaders, Data), Error>) -> Void) { - } - -} diff --git a/Frameworks/Account/AccountTests/TestTransport.swift b/Frameworks/Account/AccountTests/TestTransport.swift new file mode 100644 index 000000000..ebb9f3514 --- /dev/null +++ b/Frameworks/Account/AccountTests/TestTransport.swift @@ -0,0 +1,34 @@ +// +// TestTransport.swift +// AccountTests +// +// Created by Maurice Parker on 5/4/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import RSWeb + +final class TestTransport: Transport { + + enum TestTransportError: String, Error { + case invalidState = "The test wasn't set up correctly." + } + + var testFiles = [String: String]() + + func send(request: URLRequest, completion: @escaping (Result<(HTTPHeaders, Data), Error>) -> Void) { + + guard let urlString = request.url?.absoluteString else { + completion(.failure(TestTransportError.invalidState)) + return + } + + let testFileName = testFiles[urlString]! + let testFileURL = Bundle(for: TestTransport.self).resourceURL!.appendingPathComponent(testFileName) + let data = try! Data(contentsOf: testFileURL) + completion(.success((HTTPHeaders(), data))) + + } + +} diff --git a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift index a20121c21..bb71f3152 100644 --- a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift +++ b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift @@ -47,6 +47,47 @@ final class FeedbinAPICaller: NSObject { } + func retrieveTags(completionHandler completion: @escaping (Result<[FeedbinTag], Error>) -> Void) { + + let callURL = feedbinBaseURL.appendingPathComponent("tags.json") + let conditionalGet = accountMetadata?.conditionalGetInfo[AccountMetadata.ConditionalGetKeys.tags] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + transport.send(request: request, resultType: [FeedbinTag].self) { [weak self] result in + switch result { + case .success(let (headers, tags)): + self?.storeConditionalGet(metadata: self?.accountMetadata, key: AccountMetadata.ConditionalGetKeys.tags, headers: headers) + completion(.success(tags)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func retrieveTaggings(completionHandler completion: @escaping (Result<[FeedbinTagging], Error>) -> Void) { + + let callURL = feedbinBaseURL.appendingPathComponent("taggings.json") + let conditionalGet = accountMetadata?.conditionalGetInfo[AccountMetadata.ConditionalGetKeys.taggings] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + transport.send(request: request, resultType: [FeedbinTagging].self) { [weak self] result in + switch result { + case .success(let (headers, taggings)): + + self?.storeConditionalGet(metadata: self?.accountMetadata, key: AccountMetadata.ConditionalGetKeys.taggings, headers: headers) + + // TODO: Add paging code + + case .failure(let error): + completion(.failure(error)) + } + + } + + } + func retrieveSubscriptions(completionHandler completion: @escaping (Result<[FeedbinFeed], Error>) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json") diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index 664b0145a..fe99d9ce0 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -6,7 +6,12 @@ // Copyright © 2019 Ranchero Software, LLC. All rights reserved. // -import Foundation +#if os(macOS) +import AppKit +#else +import UIKit +import RSCore +#endif import RSWeb final class FeedbinAccountDelegate: AccountDelegate { @@ -33,18 +38,32 @@ final class FeedbinAccountDelegate: AccountDelegate { var refreshProgress = DownloadProgress(numberOfTasks: 0) - static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler handler: @escaping (Result) -> Void) { + static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler completion: @escaping (Result) -> Void) { let caller = FeedbinAPICaller(transport: transport) caller.credentials = credentials caller.validateCredentials() { result in - handler(result) + completion(result) } } - func refreshAll(for account: Account) { - + func refreshAll(for account: Account, completionHandler completion: (() -> Void)? = nil) { + refreshAll(account) { result in + switch result { + case .success(): + completion?() + case .failure(let error): + // TODO: We should do a better job of error handling here. + // We need to prompt for credentials and provide user friendly + // errors. + #if os(macOS) + NSApplication.shared.presentError(error) + #else + UIApplication.shared.presentError(error) + #endif + } + } } func accountDidInitialize(_ account: Account) { @@ -52,3 +71,67 @@ final class FeedbinAccountDelegate: AccountDelegate { } } + +// MARK: Private + +private extension FeedbinAccountDelegate { + + func refreshAll(_ account: Account, completion: @escaping (Result) -> Void) { + + caller.retrieveTags { [weak self] result in + switch result { + case .success(let tags): + self?.syncFolders(account, tags) + completion(.success(())) + case .failure(let error): + self?.checkErrorOrNotModified(error, completion: completion) + } + } + + } + + func syncFolders(_ account: Account, _ tags: [FeedbinTag]) { + + let tagNames = tags.map { $0.name } + + // Delete any folders not at Feedbin + if let folders = account.folders { + folders.forEach { folder in + if !tagNames.contains(folder.name ?? "") { + account.deleteFolder(folder) + } + } + } + + let folderNames: [String] = { + if let folders = account.folders { + return folders.map { $0.name ?? "" } + } else { + return [String]() + } + }() + + // Make any folders Feedbin has, but we don't + tagNames.forEach { tagName in + if !folderNames.contains(tagName) { + account.ensureFolder(with: tagName) + } + } + + } + + func checkErrorOrNotModified(_ error: Error, completion: @escaping (Result) -> Void) { + switch error { + case TransportError.httpError(let status): + if status == HTTPResponseCode.notModified { + completion(.success(())) + } else { + completion(.failure(error)) + } + default: + completion(.failure(error)) + } + + } + +} diff --git a/Frameworks/Account/Feedbin/FeedbinTag.swift b/Frameworks/Account/Feedbin/FeedbinTag.swift new file mode 100644 index 000000000..e603465f9 --- /dev/null +++ b/Frameworks/Account/Feedbin/FeedbinTag.swift @@ -0,0 +1,21 @@ +// +// FeedbinTag.swift +// Account +// +// Created by Maurice Parker on 5/5/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct FeedbinTag: Codable, Equatable, Hashable { + + let tagID: Int + let name: String + + enum CodingKeys: String, CodingKey { + case tagID = "id" + case name = "name" + } + +} diff --git a/Frameworks/Account/Feedbin/FeedbinTagging.swift b/Frameworks/Account/Feedbin/FeedbinTagging.swift index 3de1d0dc7..66ce2335a 100644 --- a/Frameworks/Account/Feedbin/FeedbinTagging.swift +++ b/Frameworks/Account/Feedbin/FeedbinTagging.swift @@ -8,68 +8,16 @@ import Foundation -struct FeedbinTagging: Hashable { - - // https://github.com/feedbin/feedbin-api/blob/master/content/taggings.md - // - // [ - // { - // "id": 4, - // "feed_id": 1, - // "name": "Tech" - // }, - // { - // "id": 5, - // "feed_id": 2, - // "name": "News" - // } - // ] +struct FeedbinTagging: Codable, Equatable, Hashable { let taggingID: Int let feedID: Int let name: String - private struct Key { - static let taggingID = "id" - static let feedID = "feed_id" - static let name = "name" + enum CodingKeys: String, CodingKey { + case taggingID = "id" + case feedID = "feed_id" + case name = "name" } - init?(jsonDictionary: [String: Any]) { - guard let taggingID = jsonDictionary[Key.taggingID] as? Int else { - return nil - } - guard let feedID = jsonDictionary[Key.feedID] as? Int else { - return nil - } - guard let name = jsonDictionary[Key.name] as? String else { - return nil - } - self.taggingID = taggingID - self.feedID = feedID - self.name = name - } - - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - hasher.combine(taggingID) - } - - // MARK: - Equatable - - public static func ==(lhs: FeedbinTagging, rhs: FeedbinTagging) -> Bool { - return lhs.taggingID == rhs.taggingID && lhs.feedID == rhs.feedID && lhs.name == rhs.name - } - - static func taggings(with jsonArray: [Any]) -> Set { - - let taggingsArray = jsonArray.compactMap { (item) -> FeedbinTagging? in - if let oneDictionary = item as? [String: Any] { - return FeedbinTagging(jsonDictionary: oneDictionary) - } - return nil - } - return Set(taggingsArray) - } } diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index 3d3599060..f0368ead4 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -26,9 +26,10 @@ final class LocalAccountDelegate: AccountDelegate { return handler(.success(false)) } - func refreshAll(for account: Account) { - + // LocalAccountDelegate doesn't wait for completion before calling the completion block + func refreshAll(for account: Account, completionHandler completion: (() -> Void)? = nil) { refresher.refreshFeeds(account.flattenedFeeds()) + completion?() } func accountDidInitialize(_ account: Account) { diff --git a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift index f58891d1f..33b4b8375 100644 --- a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift @@ -78,13 +78,18 @@ class AccountsFeedbinWindowController: NSWindowController { if authenticated { + var newAccount = false if self.account == nil { self.account = AccountManager.shared.createAccount(type: .feedbin) + newAccount = true } do { try self.account?.removeBasicCredentials() try self.account?.storeCredentials(credentials) + if newAccount { + self.account?.refreshAll() + } self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) } catch { self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")