8000 Use immutable instead of mutable Swift data by rgoldberg · Pull Request #687 · mas-cli/mas · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Use immutable instead of mutable Swift data #687

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 30, 2024
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
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
Loading
0