diff --git a/Frameworks/Account/Feedly/FeedlyAPICaller.swift b/Frameworks/Account/Feedly/FeedlyAPICaller.swift index 26988165b..68ad350b3 100644 --- a/Frameworks/Account/Feedly/FeedlyAPICaller.swift +++ b/Frameworks/Account/Feedly/FeedlyAPICaller.swift @@ -234,6 +234,141 @@ final class FeedlyAPICaller { } } } + + func createCollection(named label: String, completionHandler: @escaping (Result) -> ()) { + guard let accessToken = credentials?.secret else { + return DispatchQueue.main.async { + completionHandler(.failure(CredentialsError.incompleteCredentials)) + } + } + var components = baseUrlComponents + components.path = "/v3/collections" + + guard let url = components.url else { + fatalError("\(components) does not produce a valid URL.") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) + request.addValue("application/json", forHTTPHeaderField: "Accept-Type") + request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) + + do { + struct CreateCollectionBody: Encodable { + var label: String + } + let encoder = JSONEncoder() + let data = try encoder.encode(CreateCollectionBody(label: label)) + request.httpBody = data + } catch { + return DispatchQueue.main.async { + completionHandler(.failure(error)) + } + } + + transport.send(request: request, resultType: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + switch result { + case .success(let (httpResponse, collections)): + if httpResponse.statusCode == 200, let collection = collections?.first { + completionHandler(.success(collection)) + } else { + completionHandler(.failure(URLError(.cannotDecodeContentData))) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } + + func renameCollection(with id: String, to name: String, completionHandler: @escaping (Result) -> ()) { + guard let accessToken = credentials?.secret else { + return DispatchQueue.main.async { + completionHandler(.failure(CredentialsError.incompleteCredentials)) + } + } + var components = baseUrlComponents + components.path = "/v3/collections" + + guard let url = components.url else { + fatalError("\(components) does not produce a valid URL.") + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) + request.addValue("application/json", forHTTPHeaderField: "Accept-Type") + request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) + + do { + struct RenameCollectionBody: Encodable { + var id: String + var label: String + } + let encoder = JSONEncoder() + let data = try encoder.encode(RenameCollectionBody(id: id, label: name)) + request.httpBody = data + } catch { + return DispatchQueue.main.async { + completionHandler(.failure(error)) + } + } + + transport.send(request: request, resultType: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + switch result { + case .success(let (httpResponse, collections)): + if httpResponse.statusCode == 200, let collection = collections?.first { + completionHandler(.success(collection)) + } else { + completionHandler(.failure(URLError(.cannotDecodeContentData))) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } + + private func encodeForURLPath(_ pathComponent: String) -> String? { + return pathComponent.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) + } + + func deleteCollection(with id: String, completionHandler: @escaping (Result) -> ()) { + guard let accessToken = credentials?.secret else { + return DispatchQueue.main.async { + completionHandler(.failure(CredentialsError.incompleteCredentials)) + } + } + guard let encodedId = encodeForURLPath(id) else { + return DispatchQueue.main.async { + completionHandler(.failure(FeedbinAccountDelegateError.invalidParameter)) + } + } + var components = baseUrlComponents + components.percentEncodedPath = "/v3/collections/\(encodedId)" + + guard let url = components.url else { + fatalError("\(components) does not produce a valid URL.") + } + + var request = URLRequest(url: url) + request.httpMethod = "DELETE" + request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) + request.addValue("application/json", forHTTPHeaderField: "Accept-Type") + request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) + + transport.send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in + switch result { + case .success(let (httpResponse, _)): + if httpResponse.statusCode == 200 { + completionHandler(.success(())) + } else { + completionHandler(.failure(URLError(.cannotDecodeContentData))) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } } extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting { diff --git a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift index 97922ac21..cb6930069 100644 --- a/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Frameworks/Account/Feedly/FeedlyAccountDelegate.swift @@ -149,15 +149,51 @@ final class FeedlyAccountDelegate: AccountDelegate { } func addFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { - fatalError() + caller.createCollection(named: name) { result in + switch result { + case .success(let collection): + if let folder = account.ensureFolder(with: collection.label) { + folder.externalID = collection.id + completion(.success(folder)) + } else { + completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + } + case .failure(let error): + completion(.failure(error)) + } + } } func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { - fatalError() + guard let id = folder.externalID else { + completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + return + } + caller.renameCollection(with: id, to: name) { result in + switch result { + case .success(let collection): + folder.name = collection.label + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } } func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { - fatalError() + guard let id = folder.externalID else { + completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + return + } + caller.deleteCollection(with: id) { result in + switch result { + case .success: + account.removeFolder(folder) + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + } } func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/Feedly/Models/FeedlyCollection.swift b/Frameworks/Account/Feedly/Models/FeedlyCollection.swift index d66a061aa..b989c2ba3 100644 --- a/Frameworks/Account/Feedly/Models/FeedlyCollection.swift +++ b/Frameworks/Account/Feedly/Models/FeedlyCollection.swift @@ -8,7 +8,7 @@ import Foundation -struct FeedlyCollection: Decodable { +struct FeedlyCollection: Codable { var feeds: [FeedlyFeed] var label: String var id: String diff --git a/Frameworks/Account/Feedly/Models/FeedlyFeed.swift b/Frameworks/Account/Feedly/Models/FeedlyFeed.swift index 5a05f4b64..da470b78f 100644 --- a/Frameworks/Account/Feedly/Models/FeedlyFeed.swift +++ b/Frameworks/Account/Feedly/Models/FeedlyFeed.swift @@ -8,7 +8,7 @@ import Foundation -struct FeedlyFeed: Decodable { +struct FeedlyFeed: Codable { var feedId: String var id: String var title: String