mirror of
https://github.com/mas-cli/mas
synced 2024-11-22 03:23:08 +00:00
🤞🏼 Rephrase MasStoreSearch as Promises
This commit is contained in:
parent
2ad8695295
commit
a9b018854d
7 changed files with 76 additions and 103 deletions
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import Version
|
||||
|
||||
/// Manages searching the MAS catalog through the iTunes Search and Lookup APIs.
|
||||
|
@ -37,13 +38,12 @@ class MasStoreSearch: StoreSearch {
|
|||
return
|
||||
}
|
||||
|
||||
loadSearchResults(url) { results, error in
|
||||
if let error = error {
|
||||
completion(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
firstly {
|
||||
loadSearchResults(url)
|
||||
}.done { results in
|
||||
completion(results, nil)
|
||||
}.catch { error in
|
||||
completion(nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,81 +59,65 @@ class MasStoreSearch: StoreSearch {
|
|||
return
|
||||
}
|
||||
|
||||
loadSearchResults(url) { results, error in
|
||||
if let error = error {
|
||||
completion(nil, error)
|
||||
return
|
||||
}
|
||||
|
||||
completion(results?.first, nil)
|
||||
firstly {
|
||||
loadSearchResults(url)
|
||||
}.done { results in
|
||||
completion(results.first, nil)
|
||||
}.catch { error in
|
||||
completion(nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSearchResults(_ url: URL, _ completion: @escaping ([SearchResult]?, Error?) -> Void) {
|
||||
networkManager.loadData(from: url) { data, error in
|
||||
guard let data = data else {
|
||||
if let error = error {
|
||||
completion(nil, error)
|
||||
} else {
|
||||
completion(nil, MASError.noData)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var results: SearchResultList
|
||||
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
|
||||
firstly {
|
||||
networkManager.loadData(from: url)
|
||||
}.map { data -> SearchResultList in
|
||||
do {
|
||||
results = try JSONDecoder().decode(SearchResultList.self, from: data)
|
||||
return try JSONDecoder().decode(SearchResultList.self, from: data)
|
||||
} catch {
|
||||
completion(nil, MASError.jsonParsing(error: error as NSError))
|
||||
return
|
||||
throw MASError.jsonParsing(error: error as NSError)
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
for index in results.results.indices {
|
||||
let result = results.results[index]
|
||||
}.then { list -> Promise<[SearchResult]> in
|
||||
var results = list.results
|
||||
let scraping = results.indices.compactMap { index -> Guarantee<Void>? in
|
||||
let result = results[index]
|
||||
guard let searchVersion = Version(tolerant: result.version),
|
||||
let pageUrl = URL(string: result.trackViewUrl)
|
||||
else {
|
||||
continue
|
||||
return nil
|
||||
}
|
||||
|
||||
group.enter()
|
||||
self.scrapeVersionFromPage(pageUrl) { pageVersion in
|
||||
return firstly {
|
||||
self.scrapeVersionFromPage(pageUrl)
|
||||
}.done { pageVersion in
|
||||
if let pageVersion = pageVersion, pageVersion > searchVersion {
|
||||
results.results[index].version = pageVersion.description
|
||||
results[index].version = pageVersion.description
|
||||
}
|
||||
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.global()) {
|
||||
completion(results.results, nil)
|
||||
}
|
||||
return when(fulfilled: scraping).map { results }
|
||||
}
|
||||
}
|
||||
|
||||
// The App Store often lists a newer version available in an app's page than in
|
||||
// the search results. We attempt to scrape it here.
|
||||
private func scrapeVersionFromPage(_ pageUrl: URL, _ completion: @escaping (Version?) -> Void) {
|
||||
networkManager.loadData(from: pageUrl) { data, _ in
|
||||
guard let data = data else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
private func scrapeVersionFromPage(_ pageUrl: URL) -> Guarantee<Version?> {
|
||||
firstly {
|
||||
networkManager.loadData(from: pageUrl)
|
||||
}.map { data in
|
||||
let html = String(decoding: data, as: UTF8.self)
|
||||
let fullRange = NSRange(html.startIndex..<html.endIndex, in: html)
|
||||
guard let match = MasStoreSearch.versionExpression.firstMatch(in: html, range: fullRange),
|
||||
let range = Range(match.range(at: 1), in: html),
|
||||
let version = Version(tolerant: html[range])
|
||||
else {
|
||||
completion(nil)
|
||||
return
|
||||
throw MASError.noData
|
||||
}
|
||||
|
||||
completion(version)
|
||||
return version
|
||||
}.recover { _ in
|
||||
.value(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
/// Network abstraction
|
||||
class NetworkManager {
|
||||
|
@ -29,9 +30,9 @@ class NetworkManager {
|
|||
///
|
||||
/// - Parameters:
|
||||
/// - url: URL to load data from.
|
||||
/// - completionHandler: Closure where result is delivered.
|
||||
func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
session.loadData(from: url, completionHandler: completionHandler)
|
||||
/// - Returns: A Promise for the Data of the response.
|
||||
func loadData(from url: URL) -> Promise<Data> {
|
||||
session.loadData(from: url)
|
||||
}
|
||||
|
||||
/// Loads data synchronously.
|
||||
|
@ -39,26 +40,6 @@ class NetworkManager {
|
|||
/// - Parameter url: URL to load data from.
|
||||
/// - Returns: The Data of the response.
|
||||
func loadDataSync(from url: URL) throws -> Data {
|
||||
var data: Data?
|
||||
var error: Error?
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
session.loadData(from: url) {
|
||||
data = $0
|
||||
error = $1
|
||||
group.leave()
|
||||
}
|
||||
|
||||
group.wait()
|
||||
|
||||
guard error == nil else {
|
||||
throw error!
|
||||
}
|
||||
|
||||
guard data != nil else {
|
||||
throw MASError.noData
|
||||
}
|
||||
|
||||
return data!
|
||||
try session.loadData(from: url).wait()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
protocol NetworkSession {
|
||||
func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void)
|
||||
func loadData(from url: URL) -> Promise<Data>
|
||||
}
|
||||
|
|
|
@ -7,12 +7,21 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
extension URLSession: NetworkSession {
|
||||
open func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
let task = dataTask(with: url) { data, _, error in
|
||||
completionHandler(data, error)
|
||||
open func loadData(from url: URL) -> Promise<Data> {
|
||||
Promise { seal in
|
||||
dataTask(with: url) { data, _, error in
|
||||
if let data = data {
|
||||
seal.fulfill(data)
|
||||
} else if let error = error {
|
||||
seal.reject(error)
|
||||
} else {
|
||||
seal.reject(MASError.noData)
|
||||
}
|
||||
}
|
||||
.resume()
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ class NetworkManagerTests: XCTestCase {
|
|||
MasKit.initialize()
|
||||
}
|
||||
|
||||
func testSuccessfulAsyncResponse() {
|
||||
func testSuccessfulAsyncResponse() throws {
|
||||
// Setup our objects
|
||||
let session = NetworkSessionMock()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
@ -29,15 +29,8 @@ class NetworkManagerTests: XCTestCase {
|
|||
let url = URL(fileURLWithPath: "url")
|
||||
|
||||
// Perform the request and verify the result
|
||||
var response: Data?
|
||||
var error: Error?
|
||||
manager.loadData(from: url) {
|
||||
response = $0
|
||||
error = $1
|
||||
}
|
||||
|
||||
let response = try manager.loadData(from: url).wait()
|
||||
XCTAssertEqual(response, data)
|
||||
XCTAssertNil(error)
|
||||
}
|
||||
|
||||
func testSuccessfulSyncResponse() throws {
|
||||
|
@ -68,14 +61,14 @@ class NetworkManagerTests: XCTestCase {
|
|||
let url = URL(fileURLWithPath: "url")
|
||||
|
||||
// Perform the request and verify the result
|
||||
var error: Error!
|
||||
manager.loadData(from: url) { error = $1 }
|
||||
guard let masError = error as? MASError else {
|
||||
XCTFail("Error is of unexpected type.")
|
||||
return
|
||||
}
|
||||
XCTAssertThrowsError(try manager.loadData(from: url).wait()) { error in
|
||||
guard let masError = error as? MASError else {
|
||||
XCTFail("Error is of unexpected type.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(masError, MASError.noData)
|
||||
XCTAssertEqual(masError, MASError.noData)
|
||||
}
|
||||
}
|
||||
|
||||
func testFailureSyncResponse() {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import PromiseKit
|
||||
@testable import MasKit
|
||||
|
||||
/// Mock NetworkSession for testing.
|
||||
|
@ -22,7 +22,11 @@ class NetworkSessionMock: NetworkSession {
|
|||
/// - Parameters:
|
||||
/// - url: unused
|
||||
/// - completionHandler: Closure which is delivered either data or an error.
|
||||
func loadData(from _: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
completionHandler(data, error)
|
||||
func loadData(from _: URL) -> Promise<Data> {
|
||||
guard let data = data else {
|
||||
return Promise(error: error ?? MASError.noData)
|
||||
}
|
||||
|
||||
return .value(data)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
/// Mock NetworkSession for testing with saved JSON response payload files.
|
||||
class NetworkSessionMockFromFile: NetworkSessionMock {
|
||||
|
@ -25,16 +26,16 @@ class NetworkSessionMockFromFile: NetworkSessionMock {
|
|||
/// - Parameters:
|
||||
/// - url: unused
|
||||
/// - completionHandler: Closure which is delivered either data or an error.
|
||||
override func loadData(from _: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
override func loadData(from _: URL) -> Promise<Data> {
|
||||
guard let fileURL = Bundle.url(for: responseFile)
|
||||
else { fatalError("Unable to load file \(responseFile)") }
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||
completionHandler(data, nil)
|
||||
return .value(data)
|
||||
} catch {
|
||||
print("Error opening file: \(error)")
|
||||
completionHandler(nil, error)
|
||||
return Promise(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue