diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index fd4a2253b..57a779e32 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -72,22 +72,21 @@ private func downloadApp(withAppID appID: AppID, purchasing: Bool, withAttemptCo private func downloadApp(withAppID appID: AppID, purchasing: Bool = false) async throws { let purchase = await SSPurchase(appID: appID, purchasing: purchasing) _ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - CKPurchaseController.shared() - .perform(purchase, withOptions: 0) { _, _, error, response in - if let error { - continuation.resume(throwing: MASError.purchaseFailed(error: error as NSError)) - } else if response?.downloads.isEmpty == false { - Task { - do { - try await PurchaseDownloadObserver(appID: appID).observeDownloadQueue() - continuation.resume() - } catch { - continuation.resume(throwing: MASError.purchaseFailed(error: error as NSError)) - } + CKPurchaseController.shared().perform(purchase, withOptions: 0) { _, _, error, response in + if let error { + continuation.resume(throwing: MASError.purchaseFailed(error: error as NSError)) + } else if response?.downloads.isEmpty == false { + Task { + do { + try await PurchaseDownloadObserver(appID: appID).observeDownloadQueue() + continuation.resume() + } catch { + continuation.resume(throwing: MASError.purchaseFailed(error: error as NSError)) } - } else { - continuation.resume(throwing: MASError.noDownloads) } + } else { + continuation.resume(throwing: MASError.noDownloads) } + } } } diff --git a/Sources/mas/AppStore/SSPurchase.swift b/Sources/mas/AppStore/SSPurchase.swift index 4a96dd02c..a3cf0abc7 100644 --- a/Sources/mas/AppStore/SSPurchase.swift +++ b/Sources/mas/AppStore/SSPurchase.swift @@ -30,11 +30,7 @@ extension SSPurchase { parameters["pricingParameters"] = "STDRDL" } - buyParameters = - parameters.map { key, value in - "\(key)=\(value)" - } - .joined(separator: "&") + buyParameters = parameters.map { "\($0)=\($1)" }.joined(separator: "&") itemIdentifier = appID diff --git a/Sources/mas/Commands/Config.swift b/Sources/mas/Commands/Config.swift index c27c9740f..c58e5b99d 100644 --- a/Sources/mas/Commands/Config.swift +++ b/Sources/mas/Commands/Config.swift @@ -37,8 +37,7 @@ extension MAS { swift ▁▁ \(Package.swiftVersion) region ▁ \(await isoRegion?.alpha2 ?? unknown) macos ▁▁ \( - ProcessInfo.processInfo.operatingSystemVersionString.dropFirst(8) - .replacingOccurrences(of: "Build ", with: "") + ProcessInfo.processInfo.operatingSystemVersionString.dropFirst(8).replacingOccurrences(of: "Build ", with: "") ) mac ▁▁▁▁ \(configStringValue("hw.product")) cpu ▁▁▁▁ \(configStringValue("machdep.cpu.brand_string")) diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index b37261db3..3f215473f 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -15,13 +15,10 @@ extension MAS { /// This is handy as many MAS titles can be long with embedded keywords. struct Lucky: AsyncParsableCommand { static let configuration = CommandConfiguration( - // swiftformat:disable indent - abstract: - """ + abstract: """ Install the first app returned from searching the Mac App Store (app must have been previously purchased) """ - // swiftformat:enable indent ) @Flag(help: "Force reinstall") diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index 69bc9c1b8..f426ed54a 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -59,28 +59,25 @@ extension MAS { installedApps: [InstalledApp], searcher: AppStoreSearcher ) async -> [(installedApp: InstalledApp, storeApp: SearchResult)] { - // swiftformat:disable indent - let apps = - appIDOrNames.isEmpty - ? installedApps - : appIDOrNames.flatMap { appIDOrName in - if let appID = AppID(appIDOrName) { - // Argument is an AppID, lookup apps by id using argument - let installedApps = installedApps.filter { $0.id == appID } - if installedApps.isEmpty { - printError(appID.unknownMessage) - } - return installedApps - } - - // Argument is not an AppID, lookup apps by name using argument - let installedApps = installedApps.filter { $0.name == appIDOrName } + let apps = appIDOrNames.isEmpty // swiftformat:disable:next indent + ? installedApps + : appIDOrNames.flatMap { appIDOrName in + if let appID = AppID(appIDOrName) { + // Argument is an AppID, lookup apps by id using argument + let installedApps = installedApps.filter { $0.id == appID } if installedApps.isEmpty { - printError("Unknown app name '", appIDOrName, "'", separator: "") + printError(appID.unknownMessage) } return installedApps } - // swiftformat:enable indent + + // Argument is not an AppID, lookup apps by name using argument + let installedApps = installedApps.filter { $0.name == appIDOrName } + if installedApps.isEmpty { + printError("Unknown app name '", appIDOrName, "'", separator: "") + } + return installedApps + } var outdatedApps = [(InstalledApp, SearchResult)]() for installedApp in apps { diff --git a/Sources/mas/Controllers/SpotlightInstalledApps.swift b/Sources/mas/Controllers/SpotlightInstalledApps.swift index 9658ea4d8..18439288d 100644 --- a/Sources/mas/Controllers/SpotlightInstalledApps.swift +++ b/Sources/mas/Controllers/SpotlightInstalledApps.swift @@ -40,22 +40,22 @@ var installedApps: [InstalledApp] { query.stop() continuation.resume( - returning: query.results - .compactMap { result in - if let item = result as? NSMetadataItem { - InstalledApp( - id: item.value(forAttribute: "kMDItemAppStoreAdamID") as? AppID ?? 0, - name: (item.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "") - .removingSuffix(".app"), - bundleID: item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String ?? "", - path: item.value(forAttribute: NSMetadataItemPathKey) as? String ?? "", - version: item.value(forAttribute: NSMetadataItemVersionKey) as? String ?? "" - ) - } else { - nil - } + returning: query.results.compactMap { result in + if let item = result as? NSMetadataItem { + InstalledApp( + id: item.value(forAttribute: "kMDItemAppStoreAdamID") as? AppID ?? 0, + name: (item.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "").removingSuffix( + ".app" + ), + bundleID: item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String ?? "", + path: item.value(forAttribute: NSMetadataItemPathKey) as? String ?? "", + version: item.value(forAttribute: NSMetadataItemVersionKey) as? String ?? "" + ) + } else { + nil } - .sorted { $0.name.caseInsensitiveCompare($1.name) == .orderedAscending } + } + .sorted { $0.name.caseInsensitiveCompare($1.name) == .orderedAscending } ) } @@ -66,10 +66,8 @@ var installedApps: [InstalledApp] { private extension String { func removingSuffix(_ suffix: Self) -> Self { - // swiftformat:disable indent - hasSuffix(suffix) + hasSuffix(suffix) // swiftformat:disable:next indent ? Self(dropLast(suffix.count)) : self - // swiftformat:enable indent } } diff --git a/Sources/mas/Formatters/AppInfoFormatter.swift b/Sources/mas/Formatters/AppInfoFormatter.swift index f99a7bc6e..8c3e02960 100644 --- a/Sources/mas/Formatters/AppInfoFormatter.swift +++ b/Sources/mas/Formatters/AppInfoFormatter.swift @@ -38,10 +38,9 @@ enum AppInfoFormatter { /// - Parameter serverDate: String containing a date in ISO-8601 format. /// - Returns: Simple date format. private static func humanReadableDate(_ serverDate: String) -> String { - ISO8601DateFormatter().date(from: serverDate) - .map { date in - ISO8601DateFormatter.string(from: date, timeZone: .current, formatOptions: [.withFullDate]) - } - ?? "" + ISO8601DateFormatter().date(from: serverDate).map { date in + ISO8601DateFormatter.string(from: date, timeZone: .current, formatOptions: [.withFullDate]) + } // swiftformat:disable:next indent + ?? "" } } diff --git a/Sources/mas/Models/InstalledApp.swift b/Sources/mas/Models/InstalledApp.swift index 604c2164e..8a4715c69 100644 --- a/Sources/mas/Models/InstalledApp.swift +++ b/Sources/mas/Models/InstalledApp.swift @@ -39,19 +39,16 @@ extension InstalledApp { // The App Store does not enforce semantic versioning, but we assume most apps follow versioning // schemes that increase numerically over time. - // swiftformat:disable indent - return - if - let semanticBundleVersion = Version(tolerant: version), - let semanticAppStoreVersion = Version(tolerant: storeApp.version) - { - semanticBundleVersion < semanticAppStoreVersion - } else { - // If a version string can't be parsed as a Semantic Version, our best effort is to - // check for equality. The only version that matters is the one in the App Store. - // https://semver.org - version != storeApp.version - } - // swiftformat:enable indent + return if + let semanticBundleVersion = Version(tolerant: version), + let semanticAppStoreVersion = Version(tolerant: storeApp.version) + { + semanticBundleVersion < semanticAppStoreVersion + } else { + // If a version string can't be parsed as a Semantic Version, our best effort is to + // check for equality. The only version that matters is the one in the App Store. + // https://semver.org + version != storeApp.version + } } } diff --git a/Tests/masTests/Commands/InfoSpec.swift b/Tests/masTests/Commands/InfoSpec.swift index b8d56a4a3..ec7657d91 100644 --- a/Tests/masTests/Commands/InfoSpec.swift +++ b/Tests/masTests/Commands/InfoSpec.swift @@ -34,8 +34,9 @@ final class InfoSpec: AsyncSpec { ) await expecta( await consequencesOf( - try await MAS.Info.parse([String(mockResult.trackId)]) - .run(searcher: MockAppStoreSearcher([mockResult.trackId: mockResult])) + try await MAS.Info.parse([String(mockResult.trackId)]).run( + searcher: MockAppStoreSearcher([mockResult.trackId: mockResult]) + ) ) ) == ( diff --git a/Tests/masTests/Commands/OutdatedSpec.swift b/Tests/masTests/Commands/OutdatedSpec.swift index c2557bdb5..f0cabdd3f 100644 --- a/Tests/masTests/Commands/OutdatedSpec.swift +++ b/Tests/masTests/Commands/OutdatedSpec.swift @@ -30,19 +30,18 @@ final class OutdatedSpec: AsyncSpec { await expecta( await consequencesOf( - try await MAS.Outdated.parse([]) - .run( - installedApps: [ - InstalledApp( - id: mockSearchResult.trackId, - name: mockSearchResult.trackName, - bundleID: "au.id.haroldchu.mac.Bandwidth", - path: "/Applications/Bandwidth+.app", - version: "1.27" - ), - ], - searcher: MockAppStoreSearcher([mockSearchResult.trackId: mockSearchResult]) - ) + try await MAS.Outdated.parse([]).run( + installedApps: [ + InstalledApp( + id: mockSearchResult.trackId, + name: mockSearchResult.trackName, + bundleID: "au.id.haroldchu.mac.Bandwidth", + path: "/Applications/Bandwidth+.app", + version: "1.27" + ), + ], + searcher: MockAppStoreSearcher([mockSearchResult.trackId: mockSearchResult]) + ) ) ) == (nil, "490461369 Bandwidth+ (1.27 -> 1.28)\n", "") diff --git a/Tests/masTests/Commands/SearchSpec.swift b/Tests/masTests/Commands/SearchSpec.swift index a9b424572..190c98892 100644 --- a/Tests/masTests/Commands/SearchSpec.swift +++ b/Tests/masTests/Commands/SearchSpec.swift @@ -23,8 +23,7 @@ final class SearchSpec: AsyncSpec { ) await expecta( await consequencesOf( - try await MAS.Search.parse(["slack"]) - .run(searcher: MockAppStoreSearcher([mockResult.trackId: mockResult])) + try await MAS.Search.parse(["slack"]).run(searcher: MockAppStoreSearcher([mockResult.trackId: mockResult])) ) ) == (nil, " 1111 slack (0.0)\n", "") diff --git a/Tests/masTests/Models/SearchResultListSpec.swift b/Tests/masTests/Models/SearchResultListSpec.swift index b3c82d19b..5416e0667 100644 --- a/Tests/masTests/Models/SearchResultListSpec.swift +++ b/Tests/masTests/Models/SearchResultListSpec.swift @@ -18,8 +18,7 @@ final class SearchResultListSpec: QuickSpec { it("can parse bbedit") { expect( consequencesOf( - try JSONDecoder().decode(SearchResultList.self, from: Data(fromResource: "search/bbedit.json")) - .resultCount + try JSONDecoder().decode(SearchResultList.self, from: Data(fromResource: "search/bbedit.json")).resultCount ) ) == (1, nil, "", "") @@ -27,8 +26,7 @@ final class SearchResultListSpec: QuickSpec { it("can parse things") { expect( consequencesOf( - try JSONDecoder().decode(SearchResultList.self, from: Data(fromResource: "search/things.json")) - .resultCount + try JSONDecoder().decode(SearchResultList.self, from: Data(fromResource: "search/things.json")).resultCount ) ) == (50, nil, "", "") diff --git a/Tests/masTests/Models/SearchResultSpec.swift b/Tests/masTests/Models/SearchResultSpec.swift index 5314ed39c..72dc24233 100644 --- a/Tests/masTests/Models/SearchResultSpec.swift +++ b/Tests/masTests/Models/SearchResultSpec.swift @@ -18,9 +18,10 @@ final class SearchResultSpec: QuickSpec { it("can parse things") { expect( consequencesOf( - try JSONDecoder() - .decode(SearchResult.self, from: Data(fromResource: "search/things-that-go-bump.json")) - .trackId + // swiftformat:disable indent + try JSONDecoder().decode(SearchResult.self, from: Data(fromResource: "search/things-that-go-bump.json")) + .trackId + // swiftformat:enable indent ) ) == (1_472_954_003, nil, "", "")