mirror of
https://github.com/mas-cli/mas
synced 2024-11-21 19:23:01 +00:00
♻️ Refactor StoreSearch into asynchronous methods
This commit is contained in:
parent
78c1541eb4
commit
4e4479feb6
4 changed files with 141 additions and 51 deletions
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue