♻️ Refactor StoreSearch into asynchronous methods

This commit is contained in:
Chris Araman 2021-04-21 15:52:53 -07:00
parent 78c1541eb4
commit 4e4479feb6
No known key found for this signature in database
GPG key ID: BB4499D9E11B61E0
4 changed files with 141 additions and 51 deletions

View file

@ -28,62 +28,90 @@ public class MasStoreSearch: StoreSearch {
/// Searches for an app. /// Searches for an app.
/// ///
/// - Parameter appName: MAS ID of 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. /// - Parameter completion: A closure that receives the search results or an Error if there is a
/// - Throws: Error if there is a problem with the network request. /// problem with the network request. List will have no records if there were no matches.
public func search(for appName: String) throws -> SearchResultList { public func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) {
guard let url = searchURL(for: appName) 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. /// Looks up app details.
/// ///
/// - Parameter appId: MAS ID of app /// - Parameter appId: MAS ID of app
/// - Returns: Search result record of app or nil if no apps match the ID. /// - Parameter completion: A closure that receives the search result record of app, or nil if no apps match the ID,
/// - Throws: Error if there is a problem with the network request. /// or an Error if there is a problem with the network request.
public func lookup(app appId: Int) throws -> SearchResult? { public func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) {
guard let url = lookupURL(forApp: appId) guard let url = lookupURL(forApp: appId)
else { throw MASError.urlEncoding } else {
completion(nil, MASError.urlEncoding)
return
}
let results = try loadSearchResults(url) loadSearchResults(url) { results, error in
guard let searchResult = results.results.first if let error = error {
else { return nil } completion(nil, error)
return
}
return searchResult completion(results?.results.first, nil)
}
} }
private func loadSearchResults(_ url: URL) throws -> SearchResultList { public func loadSearchResults(_ url: URL, _ completion: @escaping (SearchResultList?, Error?) -> Void) {
var results: SearchResultList networkManager.loadData(from: url) { result in
let data = try networkManager.loadDataSync(from: url) guard case let .success(data) = result else {
do { if case let .failure(error) = result {
results = try JSONDecoder().decode(SearchResultList.self, from: data) completion(nil, error)
} catch { } else {
throw MASError.jsonParsing(error: error as NSError) completion(nil, MASError.noData)
}
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
} }
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 // The App Store often lists a newer version available in an app's page than in

View file

@ -10,12 +10,64 @@ import Foundation
/// Protocol for searching the MAS catalog. /// Protocol for searching the MAS catalog.
public protocol StoreSearch { public protocol StoreSearch {
func lookup(app appId: Int) throws -> SearchResult? func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void)
func search(for appName: String) throws -> SearchResultList func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void)
} }
// MARK: - Common methods // MARK: - Common methods
extension StoreSearch { 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. /// Builds the search URL for an app.
/// ///
/// - Parameter appName: MAS app identifier. /// - Parameter appName: MAS app identifier.

View file

@ -11,21 +11,26 @@
class StoreSearchMock: StoreSearch { class StoreSearchMock: StoreSearch {
var apps: [Int: SearchResult] = [:] 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) } 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 // Negative numbers are invalid
if appId <= 0 { guard appId > 0 else {
throw MASError.searchFailed completion(nil, MASError.searchFailed)
return
} }
guard let result = apps[appId] guard let result = apps[appId]
else { throw MASError.noSearchResultsFound } else {
completion(nil, MASError.noSearchResultsFound)
return
}
return result completion(result, nil)
} }
func reset() { func reset() {

View file

@ -12,8 +12,13 @@ import Quick
/// Protocol minimal implementation /// Protocol minimal implementation
struct StoreSearchForTesting: StoreSearch { struct StoreSearchForTesting: StoreSearch {
func lookup(app _: Int) throws -> SearchResult? { nil } func lookup(app _: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) {
func search(for _: String) throws -> SearchResultList { SearchResultList(resultCount: 0, results: []) } completion(nil, nil)
}
func search(for _: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) {
completion(SearchResultList(resultCount: 0, results: []), nil)
}
} }
class StoreSearchSpec: QuickSpec { class StoreSearchSpec: QuickSpec {