Convert several methods to async await.

This commit is contained in:
Brent Simmons 2024-04-23 19:10:05 -07:00
parent d2ae1d3120
commit f75f67a42c
2 changed files with 135 additions and 295 deletions

View File

@ -300,78 +300,32 @@ final class FeedlyAccountDelegate: AccountDelegate {
func renameFolder(for account: Account, with folder: Folder, to name: String) async throws {
try await withCheckedThrowingContinuation { continuation in
self.renameFolder(for: account, with: folder, to: name) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
private func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
guard let id = folder.externalID else {
return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.unableToRenameFolder(folder.nameForDisplay, name)))
}
throw FeedlyAccountDelegateError.unableToRenameFolder(folder.nameForDisplay, name)
}
let nameBefore = folder.name
caller.renameCollection(with: id, to: name) { result in
switch result {
case .success(let collection):
folder.name = collection.label
completion(.success(()))
case .failure(let error):
folder.name = nameBefore
completion(.failure(error))
}
do {
let collection = try await caller.renameCollection(with: id, to: name)
folder.name = collection.label
} catch {
folder.name = nameBefore
throw error
}
folder.name = name
}
func removeFolder(for account: Account, with folder: Folder) async throws {
try await withCheckedThrowingContinuation { continuation in
self.removeFolder(for: account, with: folder) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
private func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
guard let id = folder.externalID else {
return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFolder(folder.nameForDisplay)))
}
throw FeedlyAccountDelegateError.unableToRemoveFolder(folder.nameForDisplay)
}
let progress = refreshProgress
progress.addToNumberOfTasksAndRemaining(1)
caller.deleteCollection(with: id) { result in
progress.completeTask()
switch result {
case .success:
account.removeFolder(folder: folder)
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
refreshProgress.addTask()
defer { refreshProgress.completeTask() }
try await caller.deleteCollection(with: id)
account.removeFolder(folder: folder)
}
func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool) async throws -> Feed {
@ -511,84 +465,38 @@ final class FeedlyAccountDelegate: AccountDelegate {
func removeFeed(for account: Account, with feed: Feed, from container: any Container) async throws {
try await withCheckedThrowingContinuation { continuation in
self.removeFeed(for: account, with: feed, from: container) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
private func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let folder = container as? Folder, let collectionID = folder.externalID else {
return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed.nameForDisplay)))
}
}
caller.removeFeed(feed.feedID, fromCollectionWith: collectionID) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
folder.addFeed(feed)
completion(.failure(error))
}
throw FeedlyAccountDelegateError.unableToRemoveFeed(feed.nameForDisplay)
}
try await caller.removeFeed(feed.feedID, fromCollectionWith: collectionID)
folder.removeFeed(feed)
}
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container) async throws {
try await withCheckedThrowingContinuation { continuation in
self.moveFeed(for: account, with: feed, from: from, to: to) { result in
switch result {
case .success:
continuation.resume()
case .failure(let error):
continuation.resume(throwing: error)
}
}
guard let sourceFolder = from as? Folder, let destinationFolder = to as? Folder else {
throw FeedlyAccountDelegateError.addFeedChooseFolder
}
}
// Optimistically move the feed, undoing as appropriate to the failure
sourceFolder.removeFeed(feed)
destinationFolder.addFeed(feed)
@MainActor func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
guard let from = from as? Folder, let to = to as? Folder else {
return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder))
}
do {
try await addFeed(for: account, with: feed, to: destinationFolder)
} catch {
destinationFolder.removeFeed(feed)
throw FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed.nameForDisplay, sourceFolder.nameForDisplay, destinationFolder.nameForDisplay)
}
addFeed(for: account, with: feed, to: to) { [weak self] addResult in
switch addResult {
// now that we have added the feed, remove it from the other collection
case .success:
self?.removeFeed(for: account, with: feed, from: from) { removeResult in
switch removeResult {
case .success:
completion(.success(()))
case .failure:
from.addFeed(feed)
completion(.failure(FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay)))
}
}
case .failure(let error):
from.addFeed(feed)
to.removeFeed(feed)
completion(.failure(error))
}
// Now that we have added the feed, remove it from the source folder
do {
try await removeFeed(for: account, with: feed, from: sourceFolder)
} catch {
sourceFolder.addFeed(feed)
throw FeedlyAccountDelegateError.unableToMoveFeedBetweenFolders(feed.nameForDisplay, sourceFolder.nameForDisplay, destinationFolder.nameForDisplay)
}
// optimistically move the feed, undoing as appropriate to the failure
from.removeFeed(feed)
to.addFeed(feed)
}
func restoreFeed(for account: Account, feed: Feed, container: any Container) async throws {

View File

@ -153,25 +153,11 @@ protocol FeedlyAPICallerDelegate: AnyObject {
func importOPML(_ opmlData: Data) async throws {
guard !isSuspended else {
throw TransportError.suspended
}
guard let accessToken = credentials?.secret else {
throw CredentialsError.incompleteCredentials
}
guard !isSuspended else { throw TransportError.suspended }
var components = baseURLComponents
components.path = "/v3/opml"
guard let url = components.url else {
fatalError("\(components) does not produce a valid URL.")
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
var request = try urlRequest(path: "/v3/opml", method: HTTPMethod.post, addJSONHeaders: false, addOauthToken: true)
request.addValue("text/xml", forHTTPHeaderField: HTTPRequestHeader.contentType)
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.acceptType)
request.httpBody = opmlData
let (httpResponse, _) = try await send(request: request, resultType: String.self)
@ -182,25 +168,9 @@ protocol FeedlyAPICallerDelegate: AnyObject {
func createCollection(named label: String) async throws -> FeedlyCollection {
guard !isSuspended else {
throw TransportError.suspended
}
guard let accessToken = credentials?.secret else {
throw CredentialsError.incompleteCredentials
}
guard !isSuspended else { throw TransportError.suspended }
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)
var request = try urlRequest(path: "/v3/collections", method: HTTPMethod.post, addJSONHeaders: true, addOauthToken: true)
struct CreateCollectionBody: Encodable {
var label: String
@ -216,166 +186,81 @@ protocol FeedlyAPICallerDelegate: AnyObject {
}
return collection
}
func renameCollection(with id: String, to name: String, completion: @escaping (Result<FeedlyCollection, Error>) -> ()) {
guard !isSuspended else {
return DispatchQueue.main.async {
completion(.failure(TransportError.suspended))
}
func renameCollection(with id: String, to name: String) async throws -> FeedlyCollection {
guard !isSuspended else { throw TransportError.suspended }
var request = try urlRequest(path: "/v3/collections", method: HTTPMethod.post, addJSONHeaders: true, addOauthToken: true)
struct RenameCollectionBody: Encodable {
var id: String
var label: String
}
guard let accessToken = credentials?.secret else {
return DispatchQueue.main.async {
completion(.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 {
completion(.failure(error))
}
}
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 {
completion(.success(collection))
} else {
completion(.failure(URLError(.cannotDecodeContentData)))
}
case .failure(let error):
completion(.failure(error))
}
let encoder = JSONEncoder()
let data = try encoder.encode(RenameCollectionBody(id: id, label: name))
request.httpBody = data
let (httpResponse, collections) = try await send(request: request, resultType: [FeedlyCollection].self)
guard let collection = collections?.first, httpResponse.statusCode == HTTPResponseCode.OK else {
throw URLError(.cannotDecodeContentData)
}
return collection
}
private func encodeForURLPath(_ pathComponent: String) -> String? {
return pathComponent.addingPercentEncoding(withAllowedCharacters: uriComponentAllowed)
}
func deleteCollection(with id: String, completion: @escaping (Result<Void, Error>) -> ()) {
guard !isSuspended else {
return DispatchQueue.main.async {
completion(.failure(TransportError.suspended))
}
}
guard let accessToken = credentials?.secret else {
return DispatchQueue.main.async {
completion(.failure(CredentialsError.incompleteCredentials))
}
}
func deleteCollection(with id: String) async throws {
guard !isSuspended else { throw TransportError.suspended }
guard let encodedID = encodeForURLPath(id) else {
return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.unexpectedResourceID(id)))
}
throw FeedlyAccountDelegateError.unexpectedResourceID(id)
}
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)
send(request: request, resultType: Optional<FeedlyCollection>.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
switch result {
case .success(let (httpResponse, _)):
if httpResponse.statusCode == 200 {
completion(.success(()))
} else {
completion(.failure(URLError(.cannotDecodeContentData)))
}
case .failure(let error):
completion(.failure(error))
}
let request = try urlRequest(path: "/v3/collections/\(encodedID)", method: HTTPMethod.delete, addJSONHeaders: true, addOauthToken: true)
let (httpResponse, _) = try await send(request: request, resultType: Optional<FeedlyCollection>.self)
guard httpResponse.statusCode == HTTPResponseCode.OK else {
throw URLError(.cannotDecodeContentData)
}
}
func removeFeed(_ feedId: String, fromCollectionWith collectionID: String, completion: @escaping (Result<Void, Error>) -> ()) {
guard !isSuspended else {
return DispatchQueue.main.async {
completion(.failure(TransportError.suspended))
}
}
guard let accessToken = credentials?.secret else {
return DispatchQueue.main.async {
completion(.failure(CredentialsError.incompleteCredentials))
}
}
func removeFeed(_ feedId: String, fromCollectionWith collectionID: String) async throws {
guard !isSuspended else { throw TransportError.suspended }
guard let encodedCollectionID = encodeForURLPath(collectionID) else {
return DispatchQueue.main.async {
completion(.failure(FeedlyAccountDelegateError.unexpectedResourceID(collectionID)))
}
throw FeedlyAccountDelegateError.unexpectedResourceID(collectionID)
}
var components = baseURLComponents
components.percentEncodedPath = "/v3/collections/\(encodedCollectionID)/feeds/.mdelete"
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)
do {
struct RemovableFeed: Encodable {
let id: String
}
let encoder = JSONEncoder()
let data = try encoder.encode([RemovableFeed(id: feedId)])
request.httpBody = data
} catch {
return DispatchQueue.main.async {
completion(.failure(error))
}
request.httpMethod = HTTPMethod.delete
_addJSONHeaders(&request)
try addOauthAccessToken(&request)
struct RemovableFeed: Encodable {
let id: String
}
let encoder = JSONEncoder()
let data = try encoder.encode([RemovableFeed(id: feedId)])
request.httpBody = data
// `resultType` is optional because the Feedly API has gone from returning an array of removed feeds to returning `null`.
// https://developer.feedly.com/v3/collections/#remove-multiple-feeds-from-a-personal-collection
send(request: request, resultType: Optional<[FeedlyFeed]>.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in
switch result {
case .success((let httpResponse, _)):
if httpResponse.statusCode == 200 {
completion(.success(()))
} else {
completion(.failure(URLError(.cannotDecodeContentData)))
}
case .failure(let error):
completion(.failure(error))
}
let (httpResponse, _) = try await send(request: request, resultType: Optional<[FeedlyFeed]>.self)
guard httpResponse.statusCode == HTTPResponseCode.OK else {
throw URLError(.cannotDecodeContentData)
}
}
}
@ -946,3 +831,50 @@ extension FeedlyAPICaller: FeedlyLogoutService {
}
}
}
private extension FeedlyAPICaller {
func urlRequest(path: String, method: String, addJSONHeaders: Bool, addOauthToken: Bool) throws -> URLRequest {
let url = apiURL(path)
var request = URLRequest(url: url)
request.httpMethod = method
if addJSONHeaders {
_addJSONHeaders(&request)
}
if addOauthToken {
try addOauthAccessToken(&request)
}
return request
}
func _addJSONHeaders(_ request: inout URLRequest) {
request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType)
request.addValue("application/json", forHTTPHeaderField: "Accept-Type")
}
func addOauthAccessToken(_ request: inout URLRequest) throws {
guard let accessToken = credentials?.secret else {
throw CredentialsError.incompleteCredentials
}
request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization)
}
func apiURL(_ path: String) -> URL {
var components = baseURLComponents
components.path = path
guard let url = components.url else {
fatalError("\(components) does not produce a valid URL.")
}
return url
}
}