mirror of
https://github.com/mas-cli/mas
synced 2024-11-21 19:23:01 +00:00
Refactor to allow install
& purchase
to report unknown app IDs via console instead of cryptically via a dialog.
Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
This commit is contained in:
parent
e4bc69cf5d
commit
e639341d11
18 changed files with 136 additions and 115 deletions
|
@ -10,10 +10,43 @@ import CommerceKit
|
|||
import PromiseKit
|
||||
import StoreFoundation
|
||||
|
||||
/// Downloads a list of apps, one after the other, printing progress to the console.
|
||||
/// Sequentially downloads apps, printing progress to the console.
|
||||
///
|
||||
/// Verifies that each supplied app ID is valid before attempting to download.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appIDs: The IDs of the apps to be downloaded
|
||||
/// - unverifiedAppIDs: The app IDs of the apps to be verified and downloaded.
|
||||
/// - searcher: The `AppStoreSearcher` used to verify app IDs.
|
||||
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
|
||||
/// - Returns: A `Promise` that completes when the downloads are complete. If any fail,
|
||||
/// the promise is rejected with the first error, after all remaining downloads are attempted.
|
||||
func downloadApps(
|
||||
withAppIDs unverifiedAppIDs: [AppID],
|
||||
verifiedBy searcher: AppStoreSearcher,
|
||||
purchasing: Bool = false
|
||||
) -> Promise<Void> {
|
||||
when(resolved: unverifiedAppIDs.map { searcher.lookup(appID: $0) })
|
||||
.then { results in
|
||||
downloadApps(
|
||||
withAppIDs:
|
||||
results.compactMap { result in
|
||||
switch result {
|
||||
case .fulfilled(let searchResult):
|
||||
return searchResult.trackId
|
||||
case .rejected(let error):
|
||||
printError(String(describing: error))
|
||||
return nil
|
||||
}
|
||||
},
|
||||
purchasing: purchasing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sequentially downloads apps, printing progress to the console.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appIDs: The app IDs of the apps to be downloaded.
|
||||
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
|
||||
/// - Returns: A promise that completes when the downloads are complete. If any fail,
|
||||
/// the promise is rejected with the first error, after all remaining downloads are attempted.
|
||||
|
|
|
@ -26,9 +26,7 @@ extension MAS {
|
|||
}
|
||||
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.unknownAppID(appID)
|
||||
}
|
||||
let result = try searcher.lookup(appID: appID).wait()
|
||||
|
||||
guard let url = URL(string: result.trackViewUrl) else {
|
||||
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
|
||||
|
|
|
@ -27,11 +27,7 @@ extension MAS {
|
|||
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
do {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.unknownAppID(appID)
|
||||
}
|
||||
|
||||
print(AppInfoFormatter.format(app: result))
|
||||
print(AppInfoFormatter.format(app: try searcher.lookup(appID: appID).wait()))
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
|
|
|
@ -23,10 +23,10 @@ extension MAS {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIDs = appIDs.filter { appID in
|
||||
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
|
||||
|
@ -38,7 +38,7 @@ extension MAS {
|
|||
}
|
||||
|
||||
do {
|
||||
try downloadApps(withAppIDs: appIDs).wait()
|
||||
try downloadApps(withAppIDs: appIDs, verifiedBy: searcher).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
|
|
|
@ -63,9 +63,7 @@ private func openMacAppStore() -> Promise<Void> {
|
|||
}
|
||||
|
||||
private func openInMacAppStore(pageForAppID appID: AppID, searcher: AppStoreSearcher) throws {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.runtimeError("Unknown app ID \(appID)")
|
||||
}
|
||||
let result = try searcher.lookup(appID: appID).wait()
|
||||
|
||||
guard var urlComponents = URLComponents(string: result.trackViewUrl) else {
|
||||
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
|
||||
|
|
|
@ -30,22 +30,8 @@ extension MAS {
|
|||
_ = try when(
|
||||
fulfilled:
|
||||
appLibrary.installedApps.map { installedApp in
|
||||
firstly {
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
}
|
||||
.done { storeApp in
|
||||
guard let storeApp else {
|
||||
if verbose {
|
||||
printWarning(
|
||||
"""
|
||||
Identifier \(installedApp.itemIdentifier) not found in store. \
|
||||
Was expected to identify \(installedApp.appName).
|
||||
"""
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if installedApp.isOutdatedWhenComparedTo(storeApp) {
|
||||
print(
|
||||
"""
|
||||
|
@ -55,6 +41,20 @@ extension MAS {
|
|||
)
|
||||
}
|
||||
}
|
||||
.recover { error in
|
||||
guard case MASError.unknownAppID = error else {
|
||||
throw error
|
||||
}
|
||||
|
||||
if verbose {
|
||||
printWarning(
|
||||
"""
|
||||
Identifier \(installedApp.itemIdentifier) not found in store. \
|
||||
Was expected to identify \(installedApp.appName).
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.wait()
|
||||
|
|
|
@ -20,10 +20,10 @@ extension MAS {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let appIDs = appIDs.filter { appID in
|
||||
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName {
|
||||
|
@ -35,7 +35,7 @@ extension MAS {
|
|||
}
|
||||
|
||||
do {
|
||||
try downloadApps(withAppIDs: appIDs, purchasing: true).wait()
|
||||
try downloadApps(withAppIDs: appIDs, verifiedBy: searcher, purchasing: true).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||
}
|
||||
|
|
|
@ -71,16 +71,19 @@ extension MAS {
|
|||
|
||||
let promises = apps.map { installedApp in
|
||||
// only upgrade apps whose local version differs from the store version
|
||||
firstly {
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
}
|
||||
.map { result -> (SoftwareProduct, SearchResult)? in
|
||||
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
||||
.map { storeApp -> (SoftwareProduct, SearchResult)? in
|
||||
guard installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return (installedApp, storeApp)
|
||||
}
|
||||
.recover { error -> Promise<(SoftwareProduct, SearchResult)?> in
|
||||
guard case MASError.unknownAppID = error else {
|
||||
return Promise(error: error)
|
||||
}
|
||||
return .value(nil)
|
||||
}
|
||||
}
|
||||
|
||||
return try when(fulfilled: promises).wait().compactMap { $0 }
|
||||
|
|
|
@ -26,9 +26,7 @@ extension MAS {
|
|||
}
|
||||
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
let result = try searcher.lookup(appID: appID).wait()
|
||||
|
||||
guard let urlString = result.sellerUrl else {
|
||||
throw MASError.noVendorWebsite
|
||||
|
|
|
@ -14,9 +14,11 @@ protocol AppStoreSearcher {
|
|||
/// Looks up app details.
|
||||
///
|
||||
/// - Parameter appID: App ID.
|
||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
|
||||
/// or an `Error` if any problems occur.
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?>
|
||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
|
||||
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
|
||||
/// An `Promise` for some other `Error` if any problems occur.
|
||||
func lookup(appID: AppID) -> Promise<SearchResult>
|
||||
|
||||
/// Searches for apps.
|
||||
///
|
||||
/// - Parameter searchTerm: Term for which to search.
|
||||
|
|
|
@ -32,30 +32,27 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
|||
self.networkManager = networkManager
|
||||
}
|
||||
|
||||
/// Looks up app details.
|
||||
///
|
||||
/// - Parameter appID: App ID.
|
||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
|
||||
/// or an `Error` if any problems occur.
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?> {
|
||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
|
||||
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
|
||||
/// An `Promise` for some other `Error` if any problems occur.
|
||||
func lookup(appID: AppID) -> Promise<SearchResult> {
|
||||
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
|
||||
fatalError("Failed to build URL for \(appID)")
|
||||
}
|
||||
return firstly {
|
||||
return
|
||||
loadSearchResults(url)
|
||||
}
|
||||
.then { results -> Guarantee<SearchResult?> in
|
||||
.then { results -> Guarantee<SearchResult> in
|
||||
guard let result = results.first else {
|
||||
return .value(nil)
|
||||
throw MASError.unknownAppID(appID)
|
||||
}
|
||||
|
||||
guard let pageURL = URL(string: result.trackViewUrl) else {
|
||||
return .value(result)
|
||||
}
|
||||
|
||||
return firstly {
|
||||
return
|
||||
self.scrapeAppStoreVersion(pageURL)
|
||||
}
|
||||
.map { pageVersion in
|
||||
guard
|
||||
let pageVersion,
|
||||
|
@ -106,9 +103,7 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
|||
}
|
||||
|
||||
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
|
||||
firstly {
|
||||
networkManager.loadData(from: url)
|
||||
}
|
||||
.map { data in
|
||||
do {
|
||||
return try JSONDecoder().decode(SearchResultList.self, from: data).results
|
||||
|
@ -122,9 +117,7 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
|||
///
|
||||
/// App Store webpages frequently report a version that is newer than what is reported by the iTunes Search API.
|
||||
private func scrapeAppStoreVersion(_ pageURL: URL) -> Promise<Version?> {
|
||||
firstly {
|
||||
networkManager.loadData(from: pageURL)
|
||||
}
|
||||
.map { data in
|
||||
guard
|
||||
let html = String(data: data, encoding: .utf8),
|
||||
|
|
|
@ -26,7 +26,7 @@ public class HomeSpec: QuickSpec {
|
|||
expect {
|
||||
try MAS.Home.parse(["999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
.to(throwError(MASError.unknownAppID(999)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ public class InfoSpec: QuickSpec {
|
|||
expect {
|
||||
try MAS.Info.parse(["999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
.to(throwError(MASError.unknownAppID(999)))
|
||||
}
|
||||
it("displays app details") {
|
||||
let mockResult = SearchResult(
|
||||
|
|
|
@ -19,7 +19,7 @@ public class InstallSpec: QuickSpec {
|
|||
xdescribe("install command") {
|
||||
xit("installs apps") {
|
||||
expect {
|
||||
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary())
|
||||
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ public class OpenSpec: QuickSpec {
|
|||
expect {
|
||||
try MAS.Open.parse(["999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
.to(throwError(MASError.unknownAppID(999)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ public class PurchaseSpec: QuickSpec {
|
|||
xdescribe("purchase command") {
|
||||
xit("purchases apps") {
|
||||
expect {
|
||||
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary())
|
||||
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ public class VendorSpec: QuickSpec {
|
|||
expect {
|
||||
try MAS.Vendor.parse(["999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
.to(throwError(MASError.unknownAppID(999)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,9 +17,9 @@ class MockAppStoreSearcher: AppStoreSearcher {
|
|||
.value(apps.filter { $1.trackName.contains(searchTerm) }.map { $1 })
|
||||
}
|
||||
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?> {
|
||||
func lookup(appID: AppID) -> Promise<SearchResult> {
|
||||
guard let result = apps[appID] else {
|
||||
return Promise(error: MASError.noSearchResultsFound)
|
||||
return Promise(error: MASError.unknownAppID(appID))
|
||||
}
|
||||
|
||||
return .value(result)
|
||||
|
|
Loading…
Reference in a new issue