Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions Sources/mas/AppStore/Downloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void, Error>) 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)
}
}
}
}
6 changes: 1 addition & 5 deletions Sources/mas/AppStore/SSPurchase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 1 addition & 2 deletions Sources/mas/Commands/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
5 changes: 1 addition & 4 deletions Sources/mas/Commands/Lucky.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
33 changes: 15 additions & 18 deletions Sources/mas/Commands/Upgrade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 16 additions & 18 deletions Sources/mas/Controllers/SpotlightInstalledApps.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
)
}

Expand All @@ -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
}
}
9 changes: 4 additions & 5 deletions Sources/mas/Formatters/AppInfoFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
?? ""
}
}
25 changes: 11 additions & 14 deletions Sources/mas/Models/InstalledApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
5 changes: 3 additions & 2 deletions Tests/masTests/Commands/InfoSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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])
)
)
)
== (
Expand Down
25 changes: 12 additions & 13 deletions Tests/masTests/Commands/OutdatedSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
Expand Down
3 changes: 1 addition & 2 deletions Tests/masTests/Commands/SearchSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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", "")
Expand Down
6 changes: 2 additions & 4 deletions Tests/masTests/Models/SearchResultListSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,15 @@ 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, "", "")
}
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, "", "")
Expand Down
7 changes: 4 additions & 3 deletions Tests/masTests/Models/SearchResultSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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, "", "")
Expand Down