From 4e4479feb65f707c9d82e8c499068d9ad741068e Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 15:52:53 -0700 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20StoreSearch=20i?= =?UTF-8?q?nto=20asynchronous=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 108 +++++++++++------- MasKit/Controllers/StoreSearch.swift | 56 ++++++++- MasKitTests/Controllers/StoreSearchMock.swift | 19 +-- MasKitTests/Controllers/StoreSearchSpec.swift | 9 +- 4 files changed, 141 insertions(+), 51 deletions(-) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 582e1c6..26ab16a 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -28,62 +28,90 @@ public class MasStoreSearch: StoreSearch { /// Searches for an app. /// /// - Parameter appName: MAS ID of app - /// - Returns: Search results list of app. List will have no records if there were no matches. Never nil. - /// - Throws: Error if there is a problem with the network request. - public func search(for appName: String) throws -> SearchResultList { + /// - Parameter completion: A closure that receives the search results or an Error if there is a + /// problem with the network request. List will have no records if there were no matches. + public func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) { guard let url = searchURL(for: appName) - else { throw MASError.urlEncoding } + else { + completion(nil, MASError.urlEncoding) + return + } - return try loadSearchResults(url) + loadSearchResults(url) { results, error in + if let error = error { + completion(nil, error) + return + } + + completion(results, nil) + } } /// Looks up app details. /// /// - Parameter appId: MAS ID of app - /// - Returns: Search result record of app or nil if no apps match the ID. - /// - Throws: Error if there is a problem with the network request. - public func lookup(app appId: Int) throws -> SearchResult? { + /// - Parameter completion: A closure that receives the search result record of app, or nil if no apps match the ID, + /// or an Error if there is a problem with the network request. + public func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) { guard let url = lookupURL(forApp: appId) - else { throw MASError.urlEncoding } + else { + completion(nil, MASError.urlEncoding) + return + } - let results = try loadSearchResults(url) - guard let searchResult = results.results.first - else { return nil } + loadSearchResults(url) { results, error in + if let error = error { + completion(nil, error) + return + } - return searchResult + completion(results?.results.first, nil) + } } - private func loadSearchResults(_ url: URL) throws -> SearchResultList { - var results: SearchResultList - let data = try networkManager.loadDataSync(from: url) - do { - results = try JSONDecoder().decode(SearchResultList.self, from: data) - } catch { - throw MASError.jsonParsing(error: error as NSError) - } - - let group = DispatchGroup() - for index in results.results.indices { - let result = results.results[index] - guard let searchVersion = Version(tolerant: result.version), - let pageUrl = URL(string: result.trackViewUrl) - else { - continue - } - - group.enter() - scrapeVersionFromPage(pageUrl) { pageVersion in - if let pageVersion = pageVersion, pageVersion > searchVersion { - results.results[index].version = pageVersion.description + public func loadSearchResults(_ url: URL, _ completion: @escaping (SearchResultList?, Error?) -> Void) { + networkManager.loadData(from: url) { result in + guard case let .success(data) = result else { + if case let .failure(error) = result { + completion(nil, error) + } else { + completion(nil, MASError.noData) } - group.leave() + return + } + + var results: SearchResultList + do { + results = try JSONDecoder().decode(SearchResultList.self, from: data) + } catch { + completion(nil, MASError.jsonParsing(error: error as NSError)) + return + } + + let group = DispatchGroup() + for index in results.results.indices { + let result = results.results[index] + guard let searchVersion = Version(tolerant: result.version), + let pageUrl = URL(string: result.trackViewUrl) + else { + continue + } + + group.enter() + self.scrapeVersionFromPage(pageUrl) { pageVersion in + if let pageVersion = pageVersion, pageVersion > searchVersion { + results.results[index].version = pageVersion.description + } + + group.leave() + } + } + + group.notify(queue: DispatchQueue.global()) { + completion(results, nil) } } - - group.wait() - - return results } // The App Store often lists a newer version available in an app's page than in diff --git a/MasKit/Controllers/StoreSearch.swift b/MasKit/Controllers/StoreSearch.swift index f7e817b..2a22fda 100644 --- a/MasKit/Controllers/StoreSearch.swift +++ b/MasKit/Controllers/StoreSearch.swift @@ -10,12 +10,64 @@ import Foundation /// Protocol for searching the MAS catalog. public protocol StoreSearch { - func lookup(app appId: Int) throws -> SearchResult? - func search(for appName: String) throws -> SearchResultList + func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) + func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) } // MARK: - Common methods extension StoreSearch { + /// Looks up app details. + /// + /// - Parameter appId: MAS ID of app + /// - Returns: Search result record of app or nil if no apps match the ID. + /// - Throws: Error if there is a problem with the network request. + public func lookup(app appId: Int) throws -> SearchResult? { + var result: SearchResult? + var error: Error? + + let group = DispatchGroup() + group.enter() + lookup(app: appId) { + result = $0 + error = $1 + group.leave() + } + + group.wait() + + if let error = error { + throw error + } + + return result + } + + /// Searches for an app. + /// + /// - Parameter appName: MAS ID of app + /// - Returns: Search results list of app. List will have no records if there were no matches. Never nil. + /// - Throws: Error if there is a problem with the network request. + public func search(for appName: String) throws -> SearchResultList { + var results: SearchResultList? + var error: Error? + + let group = DispatchGroup() + group.enter() + search(for: appName) { + results = $0 + error = $1 + group.leave() + } + + group.wait() + + if let error = error { + throw error + } + + return results! + } + /// Builds the search URL for an app. /// /// - Parameter appName: MAS app identifier. diff --git a/MasKitTests/Controllers/StoreSearchMock.swift b/MasKitTests/Controllers/StoreSearchMock.swift index e899e65..bb9e45a 100644 --- a/MasKitTests/Controllers/StoreSearchMock.swift +++ b/MasKitTests/Controllers/StoreSearchMock.swift @@ -11,21 +11,26 @@ class StoreSearchMock: StoreSearch { var apps: [Int: SearchResult] = [:] - func search(for appName: String) throws -> SearchResultList { + func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) { let filtered = apps.filter { $1.trackName.contains(appName) } - return SearchResultList(resultCount: filtered.count, results: filtered.map { $1 }) + let results = SearchResultList(resultCount: filtered.count, results: filtered.map { $1 }) + completion(results, nil) } - func lookup(app appId: Int) throws -> SearchResult? { + func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) { // Negative numbers are invalid - if appId <= 0 { - throw MASError.searchFailed + guard appId > 0 else { + completion(nil, MASError.searchFailed) + return } guard let result = apps[appId] - else { throw MASError.noSearchResultsFound } + else { + completion(nil, MASError.noSearchResultsFound) + return + } - return result + completion(result, nil) } func reset() { diff --git a/MasKitTests/Controllers/StoreSearchSpec.swift b/MasKitTests/Controllers/StoreSearchSpec.swift index 87b219e..cbc54bf 100644 --- a/MasKitTests/Controllers/StoreSearchSpec.swift +++ b/MasKitTests/Controllers/StoreSearchSpec.swift @@ -12,8 +12,13 @@ import Quick /// Protocol minimal implementation struct StoreSearchForTesting: StoreSearch { - func lookup(app _: Int) throws -> SearchResult? { nil } - func search(for _: String) throws -> SearchResultList { SearchResultList(resultCount: 0, results: []) } + func lookup(app _: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) { + completion(nil, nil) + } + + func search(for _: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) { + completion(SearchResultList(resultCount: 0, results: []), nil) + } } class StoreSearchSpec: QuickSpec {