🤞🏼 Rephrase MasStoreSearch as Promises

This commit is contained in:
Chris Araman 2021-04-22 19:15:57 -07:00
parent 2ad8695295
commit a9b018854d
No known key found for this signature in database
GPG key ID: BB4499D9E11B61E0
7 changed files with 76 additions and 103 deletions

View file

@ -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)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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)
}
}

View file

@ -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)
}
}
}