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 PromiseKit
|
||||||
import StoreFoundation
|
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:
|
/// - 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.
|
/// - 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,
|
/// - 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.
|
/// 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 {
|
func run(searcher: AppStoreSearcher) throws {
|
||||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
let result = try searcher.lookup(appID: appID).wait()
|
||||||
throw MASError.unknownAppID(appID)
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let url = URL(string: result.trackViewUrl) else {
|
guard let url = URL(string: result.trackViewUrl) else {
|
||||||
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
|
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
|
||||||
|
|
|
@ -27,11 +27,7 @@ extension MAS {
|
||||||
|
|
||||||
func run(searcher: AppStoreSearcher) throws {
|
func run(searcher: AppStoreSearcher) throws {
|
||||||
do {
|
do {
|
||||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
print(AppInfoFormatter.format(app: try searcher.lookup(appID: appID).wait()))
|
||||||
throw MASError.unknownAppID(appID)
|
|
||||||
}
|
|
||||||
|
|
||||||
print(AppInfoFormatter.format(app: result))
|
|
||||||
} catch {
|
} catch {
|
||||||
throw error as? MASError ?? .searchFailed
|
throw error as? MASError ?? .searchFailed
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,10 +23,10 @@ extension MAS {
|
||||||
|
|
||||||
/// Runs the command.
|
/// Runs the command.
|
||||||
func run() throws {
|
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
|
// Try to download applications with given identifiers and collect results
|
||||||
let appIDs = appIDs.filter { appID in
|
let appIDs = appIDs.filter { appID in
|
||||||
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
|
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
|
||||||
|
@ -38,7 +38,7 @@ extension MAS {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try downloadApps(withAppIDs: appIDs).wait()
|
try downloadApps(withAppIDs: appIDs, verifiedBy: searcher).wait()
|
||||||
} catch {
|
} catch {
|
||||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
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 {
|
private func openInMacAppStore(pageForAppID appID: AppID, searcher: AppStoreSearcher) throws {
|
||||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
let result = try searcher.lookup(appID: appID).wait()
|
||||||
throw MASError.runtimeError("Unknown app ID \(appID)")
|
|
||||||
}
|
|
||||||
|
|
||||||
guard var urlComponents = URLComponents(string: result.trackViewUrl) else {
|
guard var urlComponents = URLComponents(string: result.trackViewUrl) else {
|
||||||
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
|
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")
|
||||||
|
|
|
@ -30,22 +30,8 @@ extension MAS {
|
||||||
_ = try when(
|
_ = try when(
|
||||||
fulfilled:
|
fulfilled:
|
||||||
appLibrary.installedApps.map { installedApp in
|
appLibrary.installedApps.map { installedApp in
|
||||||
firstly {
|
|
||||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||||
}
|
|
||||||
.done { storeApp in
|
.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) {
|
if installedApp.isOutdatedWhenComparedTo(storeApp) {
|
||||||
print(
|
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()
|
.wait()
|
||||||
|
|
|
@ -20,10 +20,10 @@ extension MAS {
|
||||||
|
|
||||||
/// Runs the command.
|
/// Runs the command.
|
||||||
func run() throws {
|
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
|
// Try to download applications with given identifiers and collect results
|
||||||
let appIDs = appIDs.filter { appID in
|
let appIDs = appIDs.filter { appID in
|
||||||
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName {
|
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName {
|
||||||
|
@ -35,7 +35,7 @@ extension MAS {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try downloadApps(withAppIDs: appIDs, purchasing: true).wait()
|
try downloadApps(withAppIDs: appIDs, verifiedBy: searcher, purchasing: true).wait()
|
||||||
} catch {
|
} catch {
|
||||||
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
throw error as? MASError ?? .downloadFailed(error: error as NSError)
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,16 +71,19 @@ extension MAS {
|
||||||
|
|
||||||
let promises = apps.map { installedApp in
|
let promises = apps.map { installedApp in
|
||||||
// only upgrade apps whose local version differs from the store version
|
// only upgrade apps whose local version differs from the store version
|
||||||
firstly {
|
|
||||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||||
}
|
.map { storeApp -> (SoftwareProduct, SearchResult)? in
|
||||||
.map { result -> (SoftwareProduct, SearchResult)? in
|
guard installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
||||||
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return (installedApp, storeApp)
|
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 }
|
return try when(fulfilled: promises).wait().compactMap { $0 }
|
||||||
|
|
|
@ -26,9 +26,7 @@ extension MAS {
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(searcher: AppStoreSearcher) throws {
|
func run(searcher: AppStoreSearcher) throws {
|
||||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
let result = try searcher.lookup(appID: appID).wait()
|
||||||
throw MASError.noSearchResultsFound
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let urlString = result.sellerUrl else {
|
guard let urlString = result.sellerUrl else {
|
||||||
throw MASError.noVendorWebsite
|
throw MASError.noVendorWebsite
|
||||||
|
|
|
@ -14,9 +14,11 @@ protocol AppStoreSearcher {
|
||||||
/// Looks up app details.
|
/// Looks up app details.
|
||||||
///
|
///
|
||||||
/// - Parameter appID: App ID.
|
/// - Parameter appID: App ID.
|
||||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
|
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
|
||||||
/// or an `Error` if any problems occur.
|
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
|
||||||
func lookup(appID: AppID) -> Promise<SearchResult?>
|
/// An `Promise` for some other `Error` if any problems occur.
|
||||||
|
func lookup(appID: AppID) -> Promise<SearchResult>
|
||||||
|
|
||||||
/// Searches for apps.
|
/// Searches for apps.
|
||||||
///
|
///
|
||||||
/// - Parameter searchTerm: Term for which to search.
|
/// - Parameter searchTerm: Term for which to search.
|
||||||
|
|
|
@ -32,30 +32,27 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
||||||
self.networkManager = networkManager
|
self.networkManager = networkManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Looks up app details.
|
|
||||||
///
|
|
||||||
/// - Parameter appID: App ID.
|
/// - Parameter appID: App ID.
|
||||||
/// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
|
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
|
||||||
/// or an `Error` if any problems occur.
|
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
|
||||||
func lookup(appID: AppID) -> Promise<SearchResult?> {
|
/// 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 {
|
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
|
||||||
fatalError("Failed to build URL for \(appID)")
|
fatalError("Failed to build URL for \(appID)")
|
||||||
}
|
}
|
||||||
return firstly {
|
return
|
||||||
loadSearchResults(url)
|
loadSearchResults(url)
|
||||||
}
|
.then { results -> Guarantee<SearchResult> in
|
||||||
.then { results -> Guarantee<SearchResult?> in
|
|
||||||
guard let result = results.first else {
|
guard let result = results.first else {
|
||||||
return .value(nil)
|
throw MASError.unknownAppID(appID)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let pageURL = URL(string: result.trackViewUrl) else {
|
guard let pageURL = URL(string: result.trackViewUrl) else {
|
||||||
return .value(result)
|
return .value(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
return firstly {
|
return
|
||||||
self.scrapeAppStoreVersion(pageURL)
|
self.scrapeAppStoreVersion(pageURL)
|
||||||
}
|
|
||||||
.map { pageVersion in
|
.map { pageVersion in
|
||||||
guard
|
guard
|
||||||
let pageVersion,
|
let pageVersion,
|
||||||
|
@ -106,9 +103,7 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
|
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
|
||||||
firstly {
|
|
||||||
networkManager.loadData(from: url)
|
networkManager.loadData(from: url)
|
||||||
}
|
|
||||||
.map { data in
|
.map { data in
|
||||||
do {
|
do {
|
||||||
return try JSONDecoder().decode(SearchResultList.self, from: data).results
|
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.
|
/// 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?> {
|
private func scrapeAppStoreVersion(_ pageURL: URL) -> Promise<Version?> {
|
||||||
firstly {
|
|
||||||
networkManager.loadData(from: pageURL)
|
networkManager.loadData(from: pageURL)
|
||||||
}
|
|
||||||
.map { data in
|
.map { data in
|
||||||
guard
|
guard
|
||||||
let html = String(data: data, encoding: .utf8),
|
let html = String(data: data, encoding: .utf8),
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class HomeSpec: QuickSpec {
|
||||||
expect {
|
expect {
|
||||||
try MAS.Home.parse(["999"]).run(searcher: searcher)
|
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 {
|
expect {
|
||||||
try MAS.Info.parse(["999"]).run(searcher: searcher)
|
try MAS.Info.parse(["999"]).run(searcher: searcher)
|
||||||
}
|
}
|
||||||
.to(throwError(MASError.noSearchResultsFound))
|
.to(throwError(MASError.unknownAppID(999)))
|
||||||
}
|
}
|
||||||
it("displays app details") {
|
it("displays app details") {
|
||||||
let mockResult = SearchResult(
|
let mockResult = SearchResult(
|
||||||
|
|
|
@ -19,7 +19,7 @@ public class InstallSpec: QuickSpec {
|
||||||
xdescribe("install command") {
|
xdescribe("install command") {
|
||||||
xit("installs apps") {
|
xit("installs apps") {
|
||||||
expect {
|
expect {
|
||||||
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary())
|
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
|
||||||
}
|
}
|
||||||
.toNot(throwError())
|
.toNot(throwError())
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ public class OpenSpec: QuickSpec {
|
||||||
expect {
|
expect {
|
||||||
try MAS.Open.parse(["999"]).run(searcher: searcher)
|
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") {
|
xdescribe("purchase command") {
|
||||||
xit("purchases apps") {
|
xit("purchases apps") {
|
||||||
expect {
|
expect {
|
||||||
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary())
|
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
|
||||||
}
|
}
|
||||||
.toNot(throwError())
|
.toNot(throwError())
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class VendorSpec: QuickSpec {
|
||||||
expect {
|
expect {
|
||||||
try MAS.Vendor.parse(["999"]).run(searcher: searcher)
|
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 })
|
.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 {
|
guard let result = apps[appID] else {
|
||||||
return Promise(error: MASError.noSearchResultsFound)
|
return Promise(error: MASError.unknownAppID(appID))
|
||||||
}
|
}
|
||||||
|
|
||||||
return .value(result)
|
return .value(result)
|
||||||
|
|
Loading…
Reference in a new issue