♻️ 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.
///
/// - 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

View file

@ -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.

View file

@ -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() {

View file

@ -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 {