Skip to content

Commit

Permalink
Merge pull request mas-cli#687 from rgoldberg/686-immutable
Browse files Browse the repository at this point in the history
Use immutable instead of mutable Swift data
  • Loading branch information
rgoldberg authored Dec 30, 2024
2 parents 26964a8 + 75d6511 commit 48ab1ca
Show file tree
Hide file tree
Showing 29 changed files with 238 additions and 433 deletions.
5 changes: 0 additions & 5 deletions Sources/mas/AppStore/CKSoftwareMap+SoftwareMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,8 @@

import CommerceKit

// MARK: - SoftwareProduct
extension CKSoftwareMap: SoftwareMap {
func allSoftwareProducts() -> [SoftwareProduct] {
allProducts() ?? []
}

func product(for bundleIdentifier: String) -> SoftwareProduct? {
product(forBundleIdentifier: bundleIdentifier)
}
}
14 changes: 3 additions & 11 deletions Sources/mas/AppStore/PurchaseDownloadObserver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ private let downloadedPhase: Int64 = 5

@objc
class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
let purchase: SSPurchase
private let purchase: SSPurchase
var completionHandler: (() -> Void)?
var errorHandler: ((MASError) -> Void)?
var priorPhaseType: Int64?
private var priorPhaseType: Int64?

init(purchase: SSPurchase) {
self.purchase = purchase
Expand Down Expand Up @@ -99,16 +99,8 @@ func progress(_ state: ProgressState) {
}

let barLength = 60

let completeLength = Int(state.percentComplete * Float(barLength))
var bar = ""
for index in 0..<barLength {
if index < completeLength {
bar += "#"
} else {
bar += "-"
}
}
let bar = (0..<barLength).map { $0 < completeLength ? "#" : "-" }.joined()
clearLine()
print("\(bar) \(state.percentage) \(state.phase)", terminator: "")
fflush(stdout)
Expand Down
9 changes: 5 additions & 4 deletions Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Regex
import Version

/// Manages searching the MAS catalog through the iTunes Search and Lookup APIs.
class ITunesSearchAppStoreSearcher: AppStoreSearcher {
struct ITunesSearchAppStoreSearcher: AppStoreSearcher {
private static let appVersionExpression = Regex(#"\"versionDisplay\"\:\"([^\"]+)\""#)

// CommerceKit and StoreFoundation don't seem to expose the region of the Apple ID signed
Expand Down Expand Up @@ -52,7 +52,7 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
}

return
self.scrapeAppStoreVersion(pageURL)
scrapeAppStoreVersion(pageURL)
.map { pageVersion in
guard
let pageVersion,
Expand Down Expand Up @@ -81,9 +81,10 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
func search(for searchTerm: String) -> Promise<[SearchResult]> {
// Search for apps for compatible platforms, in order of preference.
// Macs with Apple Silicon can run iPad and iPhone apps.
var entities = [Entity.desktopSoftware]
#if arch(arm64)
entities += [.iPadSoftware, .iPhoneSoftware]
let entities = [Entity.desktopSoftware, .iPadSoftware, .iPhoneSoftware]
#else
let entities = [Entity.desktopSoftware]
#endif

let results = entities.map { entity in
Expand Down
1 change: 0 additions & 1 deletion Sources/mas/Controllers/SoftwareMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,4 @@
/// Somewhat analogous to CKSoftwareMap.
protocol SoftwareMap {
func allSoftwareProducts() -> [SoftwareProduct]
func product(for bundleIdentifier: String) -> SoftwareProduct?
}
23 changes: 6 additions & 17 deletions Sources/mas/Controllers/SoftwareMapAppLibrary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,17 @@ import CommerceKit
import ScriptingBridge

/// Utility for managing installed apps.
class SoftwareMapAppLibrary: AppLibrary {
/// CommerceKit's singleton manager of installed software.
private let softwareMap: SoftwareMap

struct SoftwareMapAppLibrary: AppLibrary {
/// Array of installed software products.
lazy var installedApps: [SoftwareProduct] = softwareMap.allSoftwareProducts()
.filter { product in
product.bundlePath.starts(with: "/Applications/")
}
let installedApps: [SoftwareProduct]

/// Internal initializer for providing a mock software map.
/// - Parameter softwareMap: SoftwareMap to use
init(softwareMap: SoftwareMap = CKSoftwareMap.shared()) {
self.softwareMap = softwareMap
}

/// Finds an app using a bundle identifier.
///
/// - Parameter bundleID: Bundle identifier of app.
/// - Returns: `SoftwareProduct` for app if found; `nil` otherwise.
func installedApp(forBundleID bundleID: String) -> SoftwareProduct? {
softwareMap.product(for: bundleID)
installedApps = softwareMap.allSoftwareProducts()
.filter { product in
product.bundlePath.starts(with: "/Applications/")
}
}

/// Uninstalls all apps located at any of the elements of `appPaths`.
Expand Down
29 changes: 1 addition & 28 deletions Sources/mas/Formatters/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Foundation
// A collection of output formatting helpers

/// Terminal Control Sequence Indicator.
let csi = "\u{001B}["
private let csi = "\u{001B}["

private var standardError = FileHandle.standardError

Expand Down Expand Up @@ -67,30 +67,3 @@ func clearLine() {
print("\(csi)2K\(csi)0G", terminator: "")
fflush(stdout)
}

func captureStream(
_ stream: UnsafeMutablePointer<FILE>,
encoding: String.Encoding = .utf8,
_ block: @escaping () throws -> Void
) rethrows -> String {
let originalFd = fileno(stream)
let duplicateFd = dup(originalFd)
defer {
close(duplicateFd)
}

let pipe = Pipe()
dup2(pipe.fileHandleForWriting.fileDescriptor, originalFd)

do {
defer {
fflush(stream)
dup2(duplicateFd, originalFd)
pipe.fileHandleForWriting.closeFile()
}

try block()
}

return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: encoding) ?? ""
}
50 changes: 10 additions & 40 deletions Sources/mas/Models/SearchResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,18 @@
//

struct SearchResult: Decodable {
var bundleId: String
var currentVersionReleaseDate: String
var fileSizeBytes: String
var formattedPrice: String?
var minimumOsVersion: String
var price: Double?
var sellerName: String
var sellerUrl: String?
var trackId: AppID
var trackName: String
var trackViewUrl: String
var version: String
var currentVersionReleaseDate = ""
var fileSizeBytes = "0"
var formattedPrice: String? = "0"
var minimumOsVersion = ""
var sellerName = ""
var sellerUrl: String? = ""
var trackId: AppID = 0
var trackName = ""
var trackViewUrl = ""
var version = ""

var displayPrice: String {
formattedPrice ?? "Unknown"
}

init(
bundleId: String = "",
currentVersionReleaseDate: String = "",
fileSizeBytes: String = "0",
formattedPrice: String = "0",
minimumOsVersion: String = "",
price: Double = 0.0,
sellerName: String = "",
sellerUrl: String = "",
trackId: AppID = 0,
trackName: String = "",
trackViewUrl: String = "",
version: String = ""
) {
self.bundleId = bundleId
self.currentVersionReleaseDate = currentVersionReleaseDate
self.fileSizeBytes = fileSizeBytes
self.formattedPrice = formattedPrice
self.minimumOsVersion = minimumOsVersion
self.price = price
self.sellerName = sellerName
self.sellerUrl = sellerUrl
self.trackId = trackId
self.trackName = trackName
self.trackViewUrl = trackViewUrl
self.version = version
}
}
2 changes: 1 addition & 1 deletion Sources/mas/Network/NetworkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Foundation
import PromiseKit

/// Network abstraction.
class NetworkManager {
struct NetworkManager {
private let session: NetworkSession

/// Designated initializer.
Expand Down
7 changes: 1 addition & 6 deletions Tests/masTests/Commands/HomeSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,13 @@ import Quick

public class HomeSpec: QuickSpec {
override public func spec() {
let searcher = MockAppStoreSearcher()

beforeSuite {
MAS.initialize()
}
describe("home command") {
beforeEach {
searcher.reset()
}
it("can't find app with unknown ID") {
expect {
try MAS.Home.parse(["999"]).run(searcher: searcher)
try MAS.Home.parse(["999"]).run(searcher: MockAppStoreSearcher())
}
.to(throwError(MASError.unknownAppID(999)))
}
Expand Down
11 changes: 3 additions & 8 deletions Tests/masTests/Commands/InfoSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,13 @@ import Quick

public class InfoSpec: QuickSpec {
override public func spec() {
let searcher = MockAppStoreSearcher()

beforeSuite {
MAS.initialize()
}
describe("Info command") {
beforeEach {
searcher.reset()
}
it("can't find app with unknown ID") {
expect {
try MAS.Info.parse(["999"]).run(searcher: searcher)
try MAS.Info.parse(["999"]).run(searcher: MockAppStoreSearcher())
}
.to(throwError(MASError.unknownAppID(999)))
}
Expand All @@ -41,10 +36,10 @@ public class InfoSpec: QuickSpec {
trackViewUrl: "https://awesome.app",
version: "1.0"
)
searcher.apps[mockResult.trackId] = mockResult
expect {
try captureStream(stdout) {
try MAS.Info.parse([String(mockResult.trackId)]).run(searcher: searcher)
try MAS.Info.parse([String(mockResult.trackId)])
.run(searcher: MockAppStoreSearcher([mockResult.trackId: mockResult]))
}
}
== """
Expand Down
7 changes: 1 addition & 6 deletions Tests/masTests/Commands/OpenSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,13 @@ import Quick

public class OpenSpec: QuickSpec {
override public func spec() {
let searcher = MockAppStoreSearcher()

beforeSuite {
MAS.initialize()
}
describe("open command") {
beforeEach {
searcher.reset()
}
it("can't find app with unknown ID") {
expect {
try MAS.Open.parse(["999"]).run(searcher: searcher)
try MAS.Open.parse(["999"]).run(searcher: MockAppStoreSearcher())
}
.to(throwError(MASError.unknownAppID(999)))
}
Expand Down
28 changes: 13 additions & 15 deletions Tests/masTests/Commands/OutdatedSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,32 @@ public class OutdatedSpec: QuickSpec {
it("displays apps with pending updates") {
let mockSearchResult =
SearchResult(
bundleId: "au.id.haroldchu.mac.Bandwidth",
currentVersionReleaseDate: "2024-09-02T00:27:00Z",
fileSizeBytes: "998130",
minimumOsVersion: "10.13",
price: 0,
sellerName: "Harold Chu",
sellerUrl: "https://example.com",
trackId: 490_461_369,
trackName: "Bandwidth+",
trackViewUrl: "https://apps.apple.com/us/app/bandwidth/id490461369?mt=12&uo=4",
version: "1.28"
)
let searcher = MockAppStoreSearcher()
searcher.apps[mockSearchResult.trackId] = mockSearchResult

let mockAppLibrary = MockAppLibrary()
mockAppLibrary.installedApps.append(
MockSoftwareProduct(
appName: mockSearchResult.trackName,
bundleIdentifier: mockSearchResult.bundleId,
bundlePath: "/Applications/Bandwidth+.app",
bundleVersion: "1.27",
itemIdentifier: NSNumber(value: mockSearchResult.trackId)
)
)
expect {
try captureStream(stdout) {
try MAS.Outdated.parse([]).run(appLibrary: mockAppLibrary, searcher: searcher)
try MAS.Outdated.parse([])
.run(
appLibrary: MockAppLibrary(
MockSoftwareProduct(
appName: mockSearchResult.trackName,
bundleIdentifier: "au.id.haroldchu.mac.Bandwidth",
bundlePath: "/Applications/Bandwidth+.app",
bundleVersion: "1.27",
itemIdentifier: NSNumber(value: mockSearchResult.trackId)
)
),
searcher: MockAppStoreSearcher([mockSearchResult.trackId: mockSearchResult])
)
}
}
== "490461369 Bandwidth+ (1.27 -> 1.28)\n"
Expand Down
11 changes: 3 additions & 8 deletions Tests/masTests/Commands/SearchSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,28 @@ import Quick

public class SearchSpec: QuickSpec {
override public func spec() {
let searcher = MockAppStoreSearcher()

beforeSuite {
MAS.initialize()
}
describe("search command") {
beforeEach {
searcher.reset()
}
it("can find slack") {
let mockResult = SearchResult(
trackId: 1111,
trackName: "slack",
trackViewUrl: "mas preview url",
version: "0.0"
)
searcher.apps[mockResult.trackId] = mockResult
expect {
try captureStream(stdout) {
try MAS.Search.parse(["slack"]).run(searcher: searcher)
try MAS.Search.parse(["slack"])
.run(searcher: MockAppStoreSearcher([mockResult.trackId: mockResult]))
}
}
== " 1111 slack (0.0)\n"
}
it("fails when searching for nonexistent app") {
expect {
try MAS.Search.parse(["nonexistent"]).run(searcher: searcher)
try MAS.Search.parse(["nonexistent"]).run(searcher: MockAppStoreSearcher())
}
.to(throwError(MASError.noSearchResultsFound))
}
Expand Down
Loading

0 comments on commit 48ab1ca

Please sign in to comment.