mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Add folder syncing for Feedbin account
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = "<group>"; };
|
||||
5107A09C227DE77700C7C3C5 /* NilTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NilTransport.swift; sourceTree = "<group>"; };
|
||||
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
|
||||
5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = "<group>"; };
|
||||
5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = "<group>"; };
|
||||
51D58754227F53BE00900287 /* FeedbinTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinTag.swift; sourceTree = "<group>"; };
|
||||
51D58757227F630B00900287 /* tags_delete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_delete.json; sourceTree = "<group>"; };
|
||||
51D58758227F630B00900287 /* tags_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_add.json; sourceTree = "<group>"; };
|
||||
51D58759227F630B00900287 /* tags_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = tags_initial.json; sourceTree = "<group>"; };
|
||||
51D5875D227F643C00900287 /* AccountFolderSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFolderSyncTest.swift; sourceTree = "<group>"; };
|
||||
841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
|
||||
841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
|
||||
841974001F6DD1EC006346C4 /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
848935061F62485000CEBD24 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
848935101F62486800CEBD24 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
||||
84AF4EA3222CFDD100F6A800 /* AccountMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountMetadata.swift; sourceTree = "<group>"; };
|
||||
@@ -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 = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
@@ -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 = "<group>";
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
83
Frameworks/Account/AccountTests/AccountFolderSyncTest.swift
Normal file
83
Frameworks/Account/AccountTests/AccountFolderSyncTest.swift
Normal file
@@ -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)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
42
Frameworks/Account/AccountTests/JSON/tags_add.json
Normal file
42
Frameworks/Account/AccountTests/JSON/tags_add.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
34
Frameworks/Account/AccountTests/JSON/tags_delete.json
Normal file
34
Frameworks/Account/AccountTests/JSON/tags_delete.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
38
Frameworks/Account/AccountTests/JSON/tags_initial.json
Normal file
38
Frameworks/Account/AccountTests/JSON/tags_initial.json
Normal file
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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<T>(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) {
|
||||
}
|
||||
|
||||
}
|
||||
34
Frameworks/Account/AccountTests/TestTransport.swift
Normal file
34
Frameworks/Account/AccountTests/TestTransport.swift
Normal file
@@ -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)))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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<Bool, Error>) -> Void) {
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, completionHandler completion: @escaping (Result<Bool, Error>) -> 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, Error>) -> 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, Error>) -> Void) {
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if status == HTTPResponseCode.notModified {
|
||||
completion(.success(()))
|
||||
} else {
|
||||
completion(.failure(error))
|
||||
}
|
||||
default:
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
21
Frameworks/Account/Feedbin/FeedbinTag.swift
Normal file
21
Frameworks/Account/Feedbin/FeedbinTag.swift
Normal file
@@ -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"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<FeedbinTagging> {
|
||||
|
||||
let taggingsArray = jsonArray.compactMap { (item) -> FeedbinTagging? in
|
||||
if let oneDictionary = item as? [String: Any] {
|
||||
return FeedbinTagging(jsonDictionary: oneDictionary)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return Set(taggingsArray)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user