mirror of
https://github.com/mas-cli/mas
synced 2024-11-22 03:23:08 +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.
|
/// 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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue