Create typealias AppID = UInt64.

Use `AppID` everywhere appropriate.

Associated appID cleanup.

Partial #478

Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
This commit is contained in:
Ross Goldberg 2024-10-14 01:22:47 -04:00
parent 43505db37f
commit 39f77c01a9
No known key found for this signature in database
25 changed files with 40 additions and 52 deletions

View file

@ -17,7 +17,7 @@ import StoreFoundation
/// Only works for free apps. Defaults to false. /// 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.
func downloadAll(_ appIDs: [UInt64], purchase: Bool = false) -> Promise<Void> { func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise<Void> {
var firstError: Error? var firstError: Error?
return appIDs.reduce(Guarantee<Void>.value(())) { previous, appID in return appIDs.reduce(Guarantee<Void>.value(())) { previous, appID in
previous.then { previous.then {
@ -34,7 +34,7 @@ func downloadAll(_ appIDs: [UInt64], purchase: Bool = false) -> Promise<Void> {
} }
} }
private func downloadWithRetries(_ appID: UInt64, purchase: Bool = false, attempts: Int = 3) -> Promise<Void> { private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise<Void> {
SSPurchase().perform(adamId: appID, purchase: purchase) SSPurchase().perform(adamId: appID, purchase: purchase)
.recover { error -> Promise<Void> in .recover { error -> Promise<Void> in
guard attempts > 1 else { guard attempts > 1 else {

View file

@ -11,7 +11,7 @@ import PromiseKit
import StoreFoundation import StoreFoundation
extension SSPurchase { extension SSPurchase {
func perform(adamId: UInt64, purchase: Bool) -> Promise<Void> { func perform(adamId: AppID, purchase: Bool) -> Promise<Void> {
var parameters: [String: Any] = [ var parameters: [String: Any] = [
"productType": "C", "productType": "C",
"price": 0, "price": 0,

View file

@ -17,7 +17,7 @@ extension Mas {
) )
@Argument(help: "ID of app to show on MAS Preview") @Argument(help: "ID of app to show on MAS Preview")
var appId: Int var appId: AppID
/// Runs the command. /// Runs the command.
func run() throws { func run() throws {

View file

@ -18,7 +18,7 @@ extension Mas {
) )
@Argument(help: "ID of app to show info") @Argument(help: "ID of app to show info")
var appId: Int var appId: AppID
/// Runs the command. /// Runs the command.
func run() throws { func run() throws {

View file

@ -19,7 +19,7 @@ extension Mas {
@Flag(help: "force reinstall") @Flag(help: "force reinstall")
var force = false var force = false
@Argument(help: "app ID(s) to install") @Argument(help: "app ID(s) to install")
var appIds: [UInt64] var appIds: [AppID]
/// Runs the command. /// Runs the command.
func run() throws { func run() throws {

View file

@ -28,7 +28,7 @@ extension Mas {
} }
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws { func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws {
var appId: Int? var appId: AppID?
do { do {
let results = try storeSearch.search(for: appName).wait() let results = try storeSearch.search(for: appName).wait()
@ -44,7 +44,7 @@ extension Mas {
guard let identifier = appId else { fatalError() } guard let identifier = appId else { fatalError() }
try install(UInt64(identifier), appLibrary: appLibrary) try install(identifier, appLibrary: appLibrary)
} }
/// Installs an app. /// Installs an app.
@ -52,7 +52,7 @@ extension Mas {
/// - Parameters: /// - Parameters:
/// - appId: App identifier /// - appId: App identifier
/// - appLibrary: Library of installed apps /// - appLibrary: Library of installed apps
fileprivate func install(_ appId: UInt64, appLibrary: AppLibrary) throws { fileprivate func install(_ appId: AppID, appLibrary: AppLibrary) throws {
// Try to download applications with given identifiers and collect results // Try to download applications with given identifiers and collect results
if let product = appLibrary.installedApp(forId: appId), !force { if let product = appLibrary.installedApp(forId: appId), !force {
printWarning("\(product.appName) is already installed") printWarning("\(product.appName) is already installed")

View file

@ -9,7 +9,6 @@
import ArgumentParser import ArgumentParser
import Foundation import Foundation
private let markerValue = "appstore"
private let masScheme = "macappstore" private let masScheme = "macappstore"
extension Mas { extension Mas {
@ -21,7 +20,7 @@ extension Mas {
) )
@Argument(help: "the app ID") @Argument(help: "the app ID")
var appId: String = markerValue var appId: AppID?
/// Runs the command. /// Runs the command.
func run() throws { func run() throws {
@ -30,18 +29,12 @@ extension Mas {
func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws { func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws {
do { do {
if appId == markerValue { guard let appId else {
// If no app ID is given, just open the MAS GUI app // If no app ID is given, just open the MAS GUI app
try openCommand.run(arguments: masScheme + "://") try openCommand.run(arguments: masScheme + "://")
return return
} }
guard let appId = Int(appId)
else {
printError("Invalid app ID")
throw MASError.noSearchResultsFound
}
guard let result = try storeSearch.lookup(app: appId).wait() guard let result = try storeSearch.lookup(app: appId).wait()
else { else {
throw MASError.noSearchResultsFound throw MASError.noSearchResultsFound

View file

@ -33,7 +33,7 @@ extension Mas {
fulfilled: fulfilled:
appLibrary.installedApps.map { installedApp in appLibrary.installedApps.map { installedApp in
firstly { firstly {
storeSearch.lookup(app: installedApp.itemIdentifier.intValue) storeSearch.lookup(app: installedApp.itemIdentifier.uint64Value)
}.done { storeApp in }.done { storeApp in
guard let storeApp else { guard let storeApp else {
if verbose { if verbose {

View file

@ -16,7 +16,7 @@ extension Mas {
) )
@Argument(help: "app ID(s) to install") @Argument(help: "app ID(s) to install")
var appIds: [UInt64] var appIds: [AppID]
/// Runs the command. /// Runs the command.
func run() throws { func run() throws {

View file

@ -21,7 +21,7 @@ extension Mas {
@Flag(help: "dry run") @Flag(help: "dry run")
var dryRun = false var dryRun = false
@Argument(help: "ID of app to uninstall") @Argument(help: "ID of app to uninstall")
var appId: Int var appId: AppID
/// Runs the uninstall command. /// Runs the uninstall command.
func run() throws { func run() throws {
@ -29,8 +29,6 @@ extension Mas {
} }
func run(appLibrary: AppLibrary) throws { func run(appLibrary: AppLibrary) throws {
let appId = UInt64(appId)
guard let product = appLibrary.installedApp(forId: appId) else { guard let product = appLibrary.installedApp(forId: appId) else {
throw MASError.notInstalled throw MASError.notInstalled
} }

View file

@ -58,11 +58,11 @@ extension Mas {
appIds.isEmpty appIds.isEmpty
? appLibrary.installedApps ? appLibrary.installedApps
: appIds.compactMap { : appIds.compactMap {
if let appId = UInt64($0) { if let appId = AppID($0) {
// if argument a UInt64, lookup app by id using argument // if argument an AppID, lookup app by id using argument
return appLibrary.installedApp(forId: appId) return appLibrary.installedApp(forId: appId)
} else { } else {
// if argument not a UInt64, lookup app by name using argument // if argument not an AppID, lookup app by name using argument
return appLibrary.installedApp(named: $0) return appLibrary.installedApp(named: $0)
} }
} }
@ -70,7 +70,7 @@ 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 { firstly {
storeSearch.lookup(app: installedApp.itemIdentifier.intValue) storeSearch.lookup(app: installedApp.itemIdentifier.uint64Value)
}.map { result -> (SoftwareProduct, SearchResult)? in }.map { result -> (SoftwareProduct, SearchResult)? in
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else { guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
return nil return nil

View file

@ -17,7 +17,7 @@ extension Mas {
) )
@Argument(help: "the app ID to show the vendor's website") @Argument(help: "the app ID to show the vendor's website")
var appId: Int var appId: AppID
/// Runs the command. /// Runs the command.
func run() throws { func run() throws {

View file

@ -17,7 +17,7 @@ protocol AppLibrary {
/// ///
/// - Parameter forId: MAS ID for app. /// - Parameter forId: MAS ID for app.
/// - Returns: Software Product of app if found; nil otherwise. /// - Returns: Software Product of app if found; nil otherwise.
func installedApp(forId: UInt64) -> SoftwareProduct? func installedApp(forId: AppID) -> SoftwareProduct?
/// Uninstalls an app. /// Uninstalls an app.
/// ///
@ -32,7 +32,7 @@ extension AppLibrary {
/// ///
/// - Parameter forId: MAS ID for app. /// - Parameter forId: MAS ID for app.
/// - Returns: Software Product of app if found; nil otherwise. /// - Returns: Software Product of app if found; nil otherwise.
func installedApp(forId identifier: UInt64) -> SoftwareProduct? { func installedApp(forId identifier: AppID) -> SoftwareProduct? {
let appId = NSNumber(value: identifier) let appId = NSNumber(value: identifier)
return installedApps.first { $0.itemIdentifier == appId } return installedApps.first { $0.itemIdentifier == appId }
} }

View file

@ -53,7 +53,7 @@ class MasStoreSearch: StoreSearch {
} }
// Combine the results, removing any duplicates. // Combine the results, removing any duplicates.
var seenAppIDs = Set<Int>() var seenAppIDs = Set<AppID>()
return when(fulfilled: results).flatMapValues { $0 }.filterValues { result in return when(fulfilled: results).flatMapValues { $0 }.filterValues { result in
seenAppIDs.insert(result.trackId).inserted seenAppIDs.insert(result.trackId).inserted
} }
@ -64,7 +64,7 @@ class MasStoreSearch: StoreSearch {
/// - Parameter appId: MAS ID of app /// - Parameter appId: MAS ID of app
/// - Returns: A Promise for the search result record of app, or nil if no apps match the ID, /// - Returns: A Promise for the search result record of app, or nil if no apps match the ID,
/// or an Error if there is a problem with the network request. /// or an Error if there is a problem with the network request.
func lookup(app appId: Int) -> Promise<SearchResult?> { func lookup(app appId: AppID) -> Promise<SearchResult?> {
guard let url = lookupURL(forApp: appId, inCountry: country) else { guard let url = lookupURL(forApp: appId, inCountry: country) else {
fatalError("Failed to build URL for \(appId)") fatalError("Failed to build URL for \(appId)")
} }

View file

@ -11,7 +11,7 @@ import PromiseKit
/// Protocol for searching the MAS catalog. /// Protocol for searching the MAS catalog.
protocol StoreSearch { protocol StoreSearch {
func lookup(app appId: Int) -> Promise<SearchResult?> func lookup(app appId: AppID) -> Promise<SearchResult?>
func search(for appName: String) -> Promise<[SearchResult]> func search(for appName: String) -> Promise<[SearchResult]>
} }
@ -49,7 +49,7 @@ extension StoreSearch {
/// ///
/// - Parameter appId: MAS app identifier. /// - Parameter appId: MAS app identifier.
/// - Returns: URL for the lookup service or nil if appId can't be encoded. /// - Returns: URL for the lookup service or nil if appId can't be encoded.
func lookupURL(forApp appId: Int, inCountry country: String?) -> URL? { func lookupURL(forApp appId: AppID, inCountry country: String?) -> URL? {
guard var components = URLComponents(string: "https://itunes.apple.com/lookup") else { guard var components = URLComponents(string: "https://itunes.apple.com/lookup") else {
return nil return nil
} }

View file

@ -26,9 +26,9 @@ enum SearchResultFormatter {
let price = result.price ?? 0.0 let price = result.price ?? 0.0
if includePrice { if includePrice {
output += String(format: "%12d %@ $%5.2f (%@)\n", appId, appName, price, version) output += String(format: "%12lu %@ $%5.2f (%@)\n", appId, appName, price, version)
} else { } else {
output += String(format: "%12d %@ (%@)\n", appId, appName, version) output += String(format: "%12lu %@ (%@)\n", appId, appName, version)
} }
} }

View file

@ -9,6 +9,8 @@
import ArgumentParser import ArgumentParser
import PromiseKit import PromiseKit
typealias AppID = UInt64
@main @main
struct Mas: ParsableCommand { struct Mas: ParsableCommand {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(

View file

@ -14,7 +14,7 @@ struct SearchResult: Decodable {
var price: Double? var price: Double?
var sellerName: String var sellerName: String
var sellerUrl: String? var sellerUrl: String?
var trackId: Int var trackId: AppID
var trackName: String var trackName: String
var trackViewUrl: String var trackViewUrl: String
var version: String var version: String
@ -27,7 +27,7 @@ struct SearchResult: Decodable {
price: Double = 0.0, price: Double = 0.0,
sellerName: String = "", sellerName: String = "",
sellerUrl: String = "", sellerUrl: String = "",
trackId: Int = 0, trackId: AppID = 0,
trackName: String = "", trackName: String = "",
trackViewUrl: String = "", trackViewUrl: String = "",
version: String = "" version: String = ""

View file

@ -32,7 +32,7 @@ public class HomeSpec: QuickSpec {
expect { expect {
try Mas.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) try Mas.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
} }
.to(throwError(MASError.searchFailed)) .to(throwError())
} }
it("can't find app with unknown ID") { it("can't find app with unknown ID") {
expect { expect {

View file

@ -46,7 +46,7 @@ public class InfoSpec: QuickSpec {
expect { expect {
try Mas.Info.parse(["--", "-999"]).run(storeSearch: storeSearch) try Mas.Info.parse(["--", "-999"]).run(storeSearch: storeSearch)
} }
.to(throwError(MASError.searchFailed)) .to(throwError())
} }
it("can't find app with unknown ID") { it("can't find app with unknown ID") {
expect { expect {

View file

@ -33,7 +33,7 @@ public class OpenSpec: QuickSpec {
expect { expect {
try Mas.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) try Mas.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
} }
.to(throwError(MASError.searchFailed)) .to(throwError())
} }
it("can't find app with unknown ID") { it("can't find app with unknown ID") {
expect { expect {
@ -55,7 +55,7 @@ public class OpenSpec: QuickSpec {
} }
it("just opens MAS if no app specified") { it("just opens MAS if no app specified") {
expect { expect {
try Mas.Open.parse(["appstore"]).run(storeSearch: storeSearch, openCommand: openCommand) try Mas.Open.parse([]).run(storeSearch: storeSearch, openCommand: openCommand)
} }
.toNot(throwError()) .toNot(throwError())
expect(openCommand.arguments).toNot(beNil()) expect(openCommand.arguments).toNot(beNil())

View file

@ -18,7 +18,7 @@ public class UninstallSpec: QuickSpec {
Mas.initialize() Mas.initialize()
} }
describe("uninstall command") { describe("uninstall command") {
let appId = 12345 let appId: AppID = 12345
let app = SoftwareProductMock( let app = SoftwareProductMock(
appName: "Some App", appName: "Some App",
bundleIdentifier: "com.some.app", bundleIdentifier: "com.some.app",

View file

@ -32,7 +32,7 @@ public class VendorSpec: QuickSpec {
expect { expect {
try Mas.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand) try Mas.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
} }
.to(throwError(MASError.searchFailed)) .to(throwError())
} }
it("can't find app with unknown ID") { it("can't find app with unknown ID") {
expect { expect {

View file

@ -54,7 +54,7 @@ public class MasStoreSearchSpec: QuickSpec {
context("when lookup used") { context("when lookup used") {
it("can find slack") { it("can find slack") {
let appId = 803_453_959 let appId: AppID = 803_453_959
let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json") let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json")
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession))

View file

@ -11,7 +11,7 @@ import PromiseKit
@testable import mas @testable import mas
class StoreSearchMock: StoreSearch { class StoreSearchMock: StoreSearch {
var apps: [Int: SearchResult] = [:] var apps: [AppID: SearchResult] = [:]
func search(for appName: String) -> Promise<[SearchResult]> { func search(for appName: String) -> Promise<[SearchResult]> {
let filtered = apps.filter { $1.trackName.contains(appName) } let filtered = apps.filter { $1.trackName.contains(appName) }
@ -19,12 +19,7 @@ class StoreSearchMock: StoreSearch {
return .value(results) return .value(results)
} }
func lookup(app appId: Int) -> Promise<SearchResult?> { func lookup(app appId: AppID) -> Promise<SearchResult?> {
// Negative numbers are invalid
guard appId > 0 else {
return Promise(error: MASError.searchFailed)
}
guard let result = apps[appId] guard let result = apps[appId]
else { else {
return Promise(error: MASError.noSearchResultsFound) return Promise(error: MASError.noSearchResultsFound)