diff --git a/.swiftformat b/.swiftformat index 42f609e61..32062d41a 100644 --- a/.swiftformat +++ b/.swiftformat @@ -2,7 +2,7 @@ # .swiftformat # mas # -# SwiftFormat 0.61.0 +# SwiftFormat 0.61.1 # # Disabled rules (enabled by default) diff --git a/AGENTS.md b/AGENTS.md index f89731dab..eb6fd3135 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -298,8 +298,9 @@ Within this section & all subsections, `X` is a placeholder for any type name. 6. `switch` 7. Multiple `guard` 8. `if` / `else if`… / `else` -9. Forced unwrapping (`!` suffix) -10. `fatalError(_:file:line:)` +9. `preconditionFailure(_:file:line:)` +10. Forced unwrapping (`!` suffix) +11. `fatalError(_:file:line:)` #### Throwing diff --git a/Brewfile b/Brewfile index 3b93bcc5a..1f28ebc01 100644 --- a/Brewfile +++ b/Brewfile @@ -1,11 +1,11 @@ brew "actionlint" # 1.7.12 brew "editorconfig-checker" # 3.6.1 -brew "gh" # 2.91.0 +brew "gh" # 2.92.0 brew "git" # 2.54.0 brew "ipsw" # 3.1.672 brew "markdownlint-cli2" # 0.22.1 brew "periphery" if MacOS.version >= :sequoia && `/usr/bin/arch` == "arm64" # 3.7.4 brew "shellcheck" # 0.11.0 -brew "swiftformat" # 0.61.0 +brew "swiftformat" # 0.61.1 brew "swiftlint" # 0.63.2 brew "yamllint" # 1.38.0 diff --git a/Scripts/mas b/Scripts/mas index 4a414a619..aef8c31e3 100755 --- a/Scripts/mas +++ b/Scripts/mas @@ -57,7 +57,7 @@ try ( lookup|info) { { exec "${mas}" "${@}" 3>&1 1>&4 2>&5 } | "${jq}" -sr ' -def numberCommas($n): +def number_commas($n): ($n | abs | round | tostring) as $s | if $n < 0 then "-" else "" end + ([$s | while(length > 0; .[:-3]) | .[-3:]] | reverse | join(",")) ; @@ -81,7 +81,7 @@ try ( "\($key_map[$k]) \("▁" * ($max_key_length - ($key_map[$k] | length) + 1)) \( $in[$k] | if $k == "fileSizeBytes" then - numberCommas(tonumber? / 1e6 // 0) + " MB" + number_commas(tonumber? / 1e6 // 0) + " MB" elif $k == "currentVersionReleaseDate" then (fromdateiso8601? | strflocaltime("%Y-%m-%d")) // . else diff --git a/Sources/mas/AppStore/AppStoreAction+download.swift b/Sources/mas/AppStore/AppStoreAction+download.swift index 9340dc242..29942fd1b 100644 --- a/Sources/mas/AppStore/AppStoreAction+download.swift +++ b/Sources/mas/AppStore/AppStoreAction+download.swift @@ -287,7 +287,7 @@ private actor DownloadQueueObserver: CKDownloadQueueObserver { MAS.printer.notice( [action.performed.uppercasingFirst, snapshot.appNameAndVersion] - + (appFolderURL.map { ["in", $0.filePath] } ?? .init()), + + (appFolderURL.map { ["in", $0.filePath] } ?? .init()), // swiftformat:disable:this indent ) if let appFolderURL { @@ -513,7 +513,7 @@ extension PhaseType: CustomStringConvertible { case .downloaded: "Downloaded" case let .performing(action): - action.performing + action.performing.uppercasingFirst } } } diff --git a/Sources/mas/Commands/Config.swift b/Sources/mas/Commands/MAS.Config.swift similarity index 92% rename from Sources/mas/Commands/Config.swift rename to Sources/mas/Commands/MAS.Config.swift index 9c632745b..3f07c6078 100644 --- a/Sources/mas/Commands/Config.swift +++ b/Sources/mas/Commands/MAS.Config.swift @@ -1,5 +1,5 @@ // -// Config.swift +// MAS.Config.swift // mas // // Copyright © 2025 mas-cli. All rights reserved. @@ -23,8 +23,8 @@ extension MAS { func run() { outputFormatOptionGroup.info( JSON.Object( // swiftformat:disable:this wrap wrapArguments - dictionaryLiteral: // swiftformat:disable indent - ("mas", .string(version)), // swiftlint:disable vertical_parameter_alignment_on_call + dictionaryLiteral: // swiftlint:disable vertical_parameter_alignment_on_call + ("mas", .string(version)), // swiftformat:disable indent ("slice", .string(runningSliceArchitecture)), ("slices", .string(supportedSliceArchitectures.joined(separator: " "))), ("dist", .string(distribution)), @@ -102,7 +102,7 @@ private func configStringValue(_ name: String) -> String { return unknown } - return unsafe .init(cString: &buffer) // swiftformat:disable:this spaceAroundOperators + return unsafe .init(cString: &buffer) } private let unknown = "unknown" diff --git a/Sources/mas/Commands/Get.swift b/Sources/mas/Commands/MAS.Get.swift similarity index 97% rename from Sources/mas/Commands/Get.swift rename to Sources/mas/Commands/MAS.Get.swift index 58d34f203..b9997e1fd 100644 --- a/Sources/mas/Commands/Get.swift +++ b/Sources/mas/Commands/MAS.Get.swift @@ -1,5 +1,5 @@ // -// Get.swift +// MAS.Get.swift // mas // // Copyright © 2026 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/MAS.Home.swift similarity index 98% rename from Sources/mas/Commands/Home.swift rename to Sources/mas/Commands/MAS.Home.swift index a941d5a7c..7064a8db9 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/MAS.Home.swift @@ -1,5 +1,5 @@ // -// Home.swift +// MAS.Home.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/MAS.Install.swift similarity index 97% rename from Sources/mas/Commands/Install.swift rename to Sources/mas/Commands/MAS.Install.swift index 2e43fe216..f36c07f0a 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/MAS.Install.swift @@ -1,5 +1,5 @@ // -// Install.swift +// MAS.Install.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/MAS.List.swift similarity index 98% rename from Sources/mas/Commands/List.swift rename to Sources/mas/Commands/MAS.List.swift index ba5b9a16e..98a7eff9e 100644 --- a/Sources/mas/Commands/List.swift +++ b/Sources/mas/Commands/MAS.List.swift @@ -1,5 +1,5 @@ // -// List.swift +// MAS.List.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Lookup.swift b/Sources/mas/Commands/MAS.Lookup.swift similarity index 97% rename from Sources/mas/Commands/Lookup.swift rename to Sources/mas/Commands/MAS.Lookup.swift index 68b238e15..ba91541d7 100644 --- a/Sources/mas/Commands/Lookup.swift +++ b/Sources/mas/Commands/MAS.Lookup.swift @@ -1,5 +1,5 @@ // -// Lookup.swift +// MAS.Lookup.swift // mas // // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/MAS.Lucky.swift similarity index 98% rename from Sources/mas/Commands/Lucky.swift rename to Sources/mas/Commands/MAS.Lucky.swift index 2fe80f32e..f3cbb306d 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/MAS.Lucky.swift @@ -1,5 +1,5 @@ // -// Lucky.swift +// MAS.Lucky.swift // mas // // Copyright © 2017 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/MAS.Open.swift similarity index 99% rename from Sources/mas/Commands/Open.swift rename to Sources/mas/Commands/MAS.Open.swift index 725cac919..eb3f0c58b 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/MAS.Open.swift @@ -1,5 +1,5 @@ // -// Open.swift +// MAS.Open.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/MAS.Outdated.swift similarity index 98% rename from Sources/mas/Commands/Outdated.swift rename to Sources/mas/Commands/MAS.Outdated.swift index 915b33792..3e25f77b5 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/MAS.Outdated.swift @@ -1,5 +1,5 @@ // -// Outdated.swift +// MAS.Outdated.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Reset.swift b/Sources/mas/Commands/MAS.Reset.swift similarity index 96% rename from Sources/mas/Commands/Reset.swift rename to Sources/mas/Commands/MAS.Reset.swift index 76e671087..c494897f5 100644 --- a/Sources/mas/Commands/Reset.swift +++ b/Sources/mas/Commands/MAS.Reset.swift @@ -1,5 +1,5 @@ // -// Reset.swift +// MAS.Reset.swift // mas // // Copyright © 2016 mas-cli. All rights reserved. @@ -43,7 +43,7 @@ extension MAS { "/System/Library/PrivateFrameworks/AppStoreComponents.framework/Support/appstorecomponentsd", "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstoreagent", "/System/Library/PrivateFrameworks/CascadeSets.framework/Versions/A/XPCServices" - + "/SetStoreUpdateService.xpc/Contents/MacOS/SetStoreUpdateService", + + "/SetStoreUpdateService.xpc/Contents/MacOS/SetStoreUpdateService", // swiftformat:disable:this indent "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeaccountd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeassetd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storedownloadd", diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/MAS.Search.swift similarity index 98% rename from Sources/mas/Commands/Search.swift rename to Sources/mas/Commands/MAS.Search.swift index 313745cd6..acdd788d4 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/MAS.Search.swift @@ -1,5 +1,5 @@ // -// Search.swift +// MAS.Search.swift // mas // // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Seller.swift b/Sources/mas/Commands/MAS.Seller.swift similarity index 98% rename from Sources/mas/Commands/Seller.swift rename to Sources/mas/Commands/MAS.Seller.swift index 61b60f3c0..8a58663ed 100644 --- a/Sources/mas/Commands/Seller.swift +++ b/Sources/mas/Commands/MAS.Seller.swift @@ -1,5 +1,5 @@ // -// Seller.swift +// MAS.Seller.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/SignOut.swift b/Sources/mas/Commands/MAS.SignOut.swift similarity index 95% rename from Sources/mas/Commands/SignOut.swift rename to Sources/mas/Commands/MAS.SignOut.swift index cd993aa22..fec5983e5 100644 --- a/Sources/mas/Commands/SignOut.swift +++ b/Sources/mas/Commands/MAS.SignOut.swift @@ -1,5 +1,5 @@ // -// SignOut.swift +// MAS.SignOut.swift // mas // // Copyright © 2016 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/MAS.Uninstall.swift similarity index 99% rename from Sources/mas/Commands/Uninstall.swift rename to Sources/mas/Commands/MAS.Uninstall.swift index 20cb75f7e..64dd8a126 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/MAS.Uninstall.swift @@ -1,5 +1,5 @@ // -// Uninstall.swift +// MAS.Uninstall.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Update.swift b/Sources/mas/Commands/MAS.Update.swift similarity index 98% rename from Sources/mas/Commands/Update.swift rename to Sources/mas/Commands/MAS.Update.swift index 215c6fdb3..e33304391 100644 --- a/Sources/mas/Commands/Update.swift +++ b/Sources/mas/Commands/MAS.Update.swift @@ -1,5 +1,5 @@ // -// Update.swift +// MAS.Update.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Commands/Version.swift b/Sources/mas/Commands/MAS.Version.swift similarity index 93% rename from Sources/mas/Commands/Version.swift rename to Sources/mas/Commands/MAS.Version.swift index 8f09e47bf..ad0539183 100644 --- a/Sources/mas/Commands/Version.swift +++ b/Sources/mas/Commands/MAS.Version.swift @@ -1,5 +1,5 @@ // -// Version.swift +// MAS.Version.swift // mas // // Copyright © 2015 mas-cli. All rights reserved. diff --git a/Sources/mas/Models/CatalogApp.swift b/Sources/mas/Models/CatalogApp.swift index af56ce7b7..4b8257ab1 100644 --- a/Sources/mas/Models/CatalogApp.swift +++ b/Sources/mas/Models/CatalogApp.swift @@ -22,51 +22,6 @@ struct CatalogApp { private let json: Lazy - fileprivate init?(macDesktopAppObject object: JSON.Object) async throws { - guard try object["supportedDevices"]?.decode(to: [String]?.self)?.contains("MacDesktop-MacDesktop") == true else { - return nil - } - - let appStorePageURLString: String = try object["trackViewUrl"]?.decode() ?? "" - guard - let minimumOSVersion = try? await URL(string: appStorePageURLString) - .flatMap({ url in - try SwiftSoup.parse(try await Dependencies.current.dataFrom(url).0, appStorePageURLString) - .getElementById("serialized-server-data")? // swiftformat:disable:this acronyms - .data() - .query( - string: """ - $.data[0].data.shelfMapping.information.items[?(@.title == 'Compatibility')].items[?(@.heading == 'Mac')].text - """, - )? - .firstMatch(of: minimumOSVersionRegex) - .map { String($0.version) } - }) - else { - return nil - } - - var jsonObject = object - let jsonMinimumOSVersion = try jsonObject[minimumOSVersionKey]?.decode() ?? "" - if jsonMinimumOSVersion != minimumOSVersion { - if let index = jsonObject.fields.firstIndex(where: { $0.key == minimumOSVersionKey }) { - jsonObject.fields[index] = (minimumOSVersionKey, .string(minimumOSVersion)) - } else { - jsonObject.fields.append((minimumOSVersionKey, .string(minimumOSVersion))) - } - } - - self.init( - adamID: try jsonObject["trackId"]?.decode() ?? 0, - appStorePageURLString: appStorePageURLString, - minimumOSVersion: minimumOSVersion, - name: try jsonObject["trackName"]?.decode() ?? "", - sellerURLString: try jsonObject["sellerUrl"]?.decode(), - version: try jsonObject["version"]?.decode() ?? "", - jsonObject: jsonObject, - ) - } - private init( adamID: ADAMID, appStorePageURLString: String, @@ -112,6 +67,50 @@ extension CatalogApp: JSONDecodable { jsonObject: object, ) } + + fileprivate init?(macDesktopAppObject object: JSON.Object) async throws { + guard + try object["supportedDevices"]?.decode(to: [String]?.self)?.contains("MacDesktop-MacDesktop") == true, + let appStorePageURLString = try object["trackViewUrl"]?.decode(to: String?.self), + let minimumOSVersion = try? await URL(string: appStorePageURLString) + .flatMap( + { url in + try SwiftSoup.parse(try await Dependencies.current.dataFrom(url).0, appStorePageURLString) + .getElementById("serialized-server-data")? // swiftformat:disable:this acronyms + .data() + .query( + string: """ + $.data[0].data.shelfMapping.information.items[?(@.title == 'Compatibility')].items[?(@.heading == 'Mac')].text + """, + )? + .firstMatch(of: minimumOSVersionRegex) + .map { String($0.version) } + }, + ) + else { + return nil + } + + var object = object + let jsonMinimumOSVersion = try object[minimumOSVersionKey]?.decode() ?? "" + if jsonMinimumOSVersion != minimumOSVersion { + if let index = object.fields.firstIndex(where: { $0.key == minimumOSVersionKey }) { + object.fields[index] = (minimumOSVersionKey, .string(minimumOSVersion)) + } else { + object.fields.append((minimumOSVersionKey, .string(minimumOSVersion))) + } + } + + self.init( + adamID: try object["trackId"]?.decode() ?? 0, + appStorePageURLString: appStorePageURLString, + minimumOSVersion: minimumOSVersion, + name: try object["trackName"]?.decode() ?? "", + sellerURLString: try object["sellerUrl"]?.decode(), + version: try object["version"]?.decode() ?? "", + jsonObject: object, + ) + } } private extension JSON.Node { @@ -297,14 +296,26 @@ private func lookup(appID: AppID, inRegion region: Region = appStoreRegion) asyn case let .bundleID(bundleID): URLQueryItem(name: "bundleId", value: bundleID) } - return if let catalogAppJSONObject = try await catalogAppJSONObjects("lookup", queryItem, inRegion: region).first { + return if // swiftformat:disable:this wrap wrapArguments + let catalogAppJSONObject = try await catalogAppJSONObjects( + from: Dependencies.current.lookupURL, + queryItem, + inRegion: region, + ) + .first // swiftformat:disable:this indent + { try .init(object: catalogAppJSONObject) } else { - try await catalogAppJSONObjects("lookup", queryItem, inRegion: region, additionalQueryItems: .init()) - .first + try await catalogAppJSONObjects( + from: Dependencies.current.lookupURL, + queryItem, + inRegion: region, + additionalQueryItems: .init(), + ) + .first // swiftformat:disable indent .flatMap { try await CatalogApp(macDesktopAppObject: $0) } ?? { throw MASError.unknownAppID(appID) }() - } + } // swiftformat:enable indent } func search(for searchTerm: String) async throws -> [CatalogApp] { @@ -314,44 +325,42 @@ func search(for searchTerm: String) async throws -> [CatalogApp] { private func search(for searchTerm: String, inRegion region: Region = appStoreRegion) async throws -> [CatalogApp] { let queryItem = URLQueryItem(name: "term", value: searchTerm) let catalogApps = - try await catalogAppJSONObjects("search", queryItem, inRegion: region).map { try CatalogApp(object: $0) } + try await catalogAppJSONObjects(from: Dependencies.current.searchURL, queryItem, inRegion: region) + .map { try CatalogApp(object: $0) } let adamIDSet = Set(catalogApps.map(\.adamID)) return catalogApps.priorityMerge( - try await catalogAppJSONObjects("search", queryItem, inRegion: region, additionalQueryItems: .init()) - .concurrentCompactMap { catalogAppJSONObject in - guard - try catalogAppJSONObject["trackId"]?.decode(to: ADAMID?.self).map({ adamIDSet.contains($0) }) == false - else { - return nil - } - - return try await .init(macDesktopAppObject: catalogAppJSONObject) + try await catalogAppJSONObjects( + from: Dependencies.current.searchURL, + queryItem, + inRegion: region, + additionalQueryItems: .init(), + ) + .concurrentCompactMap { catalogAppJSONObject in // swiftformat:disable indent + try catalogAppJSONObject["trackId"]?.decode(to: ADAMID?.self).map { adamIDSet.contains($0) } == false + ? try await .init(macDesktopAppObject: catalogAppJSONObject) + : nil }, - ) { $0.name.similarity(to: searchTerm) } + ) { $0.name.similarity(to: searchTerm) } // swiftformat:enable indent } private func catalogAppJSONObjects( - _ action: String, + from url: URL, _ queryItem: URLQueryItem, inRegion region: Region, additionalQueryItems: [URLQueryItem] = [.init(name: "entity", value: "desktopSoftware")], ) async throws -> [JSON.Object] { - let urlString = "https://itunes.apple.com/\(action)" - return try await URL(string: urlString).map { url in - try await unsafe Dependencies.current - .dataFrom( - url.appending( - queryItems: [.init(name: "media", value: "software")] - + additionalQueryItems - + [.init(name: "country", value: region), queryItem], - ), - ) - .0 - .withUnsafeBytes { bufferPointer in - try CatalogAppResults(json: .init(parsing: unsafe RawSpan(_unsafeBytes: unsafe bufferPointer))).resultObjects - } - } - ?? { throw MASError.invalidURL(urlString) }() // swiftformat:disable:this indent + try await unsafe Dependencies.current + .dataFrom( + url.appending( + queryItems: [.init(name: "media", value: "software")] + + additionalQueryItems // swiftformat:disable:this indent + + [.init(name: "country", value: region), queryItem], // swiftformat:disable:this indent + ), + ) + .0 + .withUnsafeBytes { bufferPointer in + try CatalogAppResults(json: .init(parsing: unsafe RawSpan(_unsafeBytes: unsafe bufferPointer))).resultObjects + } } private let minimumOSVersionKey = JSON.Key("minimumOsVersion") diff --git a/Sources/mas/Models/InstalledApp.swift b/Sources/mas/Models/InstalledApp.swift index 7a8ab2a21..f3973cb2d 100644 --- a/Sources/mas/Models/InstalledApp.swift +++ b/Sources/mas/Models/InstalledApp.swift @@ -127,7 +127,7 @@ private extension JSON.Node { case let array as [Any?]: .array(.init(array.map { .init(for: $0) })) default: - value.map { .string(.init(describing: $0)) } ?? .null // swiftformat:disable:this indent + value.map { .string(.init(describing: $0)) } ?? .null } } } diff --git a/Sources/mas/Models/MASError.swift b/Sources/mas/Models/MASError.swift index 624e5e8f1..1daf23ee4 100644 --- a/Sources/mas/Models/MASError.swift +++ b/Sources/mas/Models/MASError.swift @@ -20,7 +20,7 @@ enum MASError: Error { ) -> Self { .error( message, - cause: cause.map { Self.error($0) }, // swiftformat:disable:this redundantStaticSelf + cause: cause.map { Self.error($0) }, separatorWhenCause: separatorWhenCause, separatorWhenNoCause: separatorWhenNoCause, ) diff --git a/Sources/mas/Utilities/Output/Printer.swift b/Sources/mas/Utilities/Output/Printer.swift index f161faf68..ebd6b0dc8 100644 --- a/Sources/mas/Utilities/Output/Printer.swift +++ b/Sources/mas/Utilities/Output/Printer.swift @@ -145,7 +145,7 @@ struct Printer { print( items.first.map { item in ["\(formattedPrefix) \(mas.indent(item, with: indent))"] - + items.dropFirst().map { mas.indent($0, with: indent) } + + items.dropFirst().map { mas.indent($0, with: indent) } // swiftformat:disable:this indent } ?? [formattedPrefix], // swiftformat:disable:this indent separator: mas.indent(separator, with: indent), diff --git a/Sources/mas/Utilities/Processes/Sudo.swift b/Sources/mas/Utilities/Processes/Sudo.swift index 2d4e87a0d..83c008a00 100644 --- a/Sources/mas/Utilities/Processes/Sudo.swift +++ b/Sources/mas/Utilities/Processes/Sudo.swift @@ -30,7 +30,7 @@ private func sudo(_ args: some Sequence) throws { guard spawnStatus == 0 else { throw MASError.error( "Failed to spawn installer process", - cause: unsafe .init(cString: strerror(spawnStatus)), // swiftformat:disable:this spaceAroundOperators + cause: unsafe .init(cString: strerror(spawnStatus)), separatorWhenCause: ": ", ) } diff --git a/Sources/mas/Utilities/Resources/Dependencies.swift b/Sources/mas/Utilities/Resources/Dependencies.swift index aa72e11f8..24624a4e1 100644 --- a/Sources/mas/Utilities/Resources/Dependencies.swift +++ b/Sources/mas/Utilities/Resources/Dependencies.swift @@ -11,6 +11,10 @@ struct Dependencies { @TaskLocal static var current = Self() + @Required(URL(string: "https://itunes.apple.com/lookup")) + var lookupURL: URL + @Required(URL(string: "https://itunes.apple.com/search")) + var searchURL: URL let dataFrom: @Sendable (URL) async throws -> (Data, URLResponse) let lookupAppFromAppID: @Sendable (AppID) async throws -> CatalogApp let searchForAppsMatchingSearchTerm: @Sendable (String) async throws -> [CatalogApp] diff --git a/Sources/mas/Utilities/Swift/Required.swift b/Sources/mas/Utilities/Swift/Required.swift new file mode 100644 index 000000000..93ec87733 --- /dev/null +++ b/Sources/mas/Utilities/Swift/Required.swift @@ -0,0 +1,17 @@ +// +// Required.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +@propertyWrapper +struct Required { + let wrappedValue: Value + + init(_ value: @autoclosure () -> Value?, file: StaticString = #file, line: UInt = #line) { + wrappedValue = value() ?? { preconditionFailure("Required value was nil", file: file, line: line) }() + } +} + +extension Required: Sendable where Value: Sendable {} diff --git a/Sources/mas/Utilities/Versions/CoreIntegerVersion.swift b/Sources/mas/Utilities/Versions/CoreIntegerVersion.swift new file mode 100644 index 000000000..3497b38fa --- /dev/null +++ b/Sources/mas/Utilities/Versions/CoreIntegerVersion.swift @@ -0,0 +1,22 @@ +// +// CoreIntegerVersion.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol CoreIntegerVersion: Version { + associatedtype Integer: BinaryInteger + + var coreIntegers: [Integer] { get } +} + +extension CoreIntegerVersion where Integer: FixedWidthInteger { + static func coreElements(from coreIntegers: [Integer]) -> [String] { + coreIntegers.map { .init($0) } + } + + var coreElements: [String] { + Self.coreElements(from: coreIntegers) + } +} diff --git a/Sources/mas/Utilities/Versions/MajorMinorPatch.swift b/Sources/mas/Utilities/Versions/MajorMinorPatch.swift new file mode 100644 index 000000000..088952a7d --- /dev/null +++ b/Sources/mas/Utilities/Versions/MajorMinorPatch.swift @@ -0,0 +1,12 @@ +// +// MajorMinorPatch.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol MajorMinorPatch { // swiftlint:disable unused_declaration + var major: String { get } // periphery:ignore + var minor: String { get } // periphery:ignore + var patch: String { get } // periphery:ignore +} // swiftlint:enable unused_declaration diff --git a/Sources/mas/Utilities/Versions/MajorMinorPatchInteger.swift b/Sources/mas/Utilities/Versions/MajorMinorPatchInteger.swift new file mode 100644 index 000000000..419227334 --- /dev/null +++ b/Sources/mas/Utilities/Versions/MajorMinorPatchInteger.swift @@ -0,0 +1,28 @@ +// +// MajorMinorPatchInteger.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol MajorMinorPatchInteger: MajorMinorPatch { + associatedtype Integer: BinaryInteger + + var majorInteger: Integer { get } + var minorInteger: Integer { get } + var patchInteger: Integer { get } +} + +extension MajorMinorPatchInteger { + var major: String { + .init(majorInteger) + } + + var minor: String { + .init(minorInteger) + } + + var patch: String { + .init(patchInteger) + } +} diff --git a/Sources/mas/Utilities/Versions/SemVerSyntax.swift b/Sources/mas/Utilities/Versions/SemVerSyntax.swift new file mode 100644 index 000000000..f2c469945 --- /dev/null +++ b/Sources/mas/Utilities/Versions/SemVerSyntax.swift @@ -0,0 +1,34 @@ +// +// SemVerSyntax.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol SemVerSyntax: Version {} + +extension SemVerSyntax { + static func core(from coreElements: [String]) -> String { + coreElements.joined(separator: ".") + } + + static func prerelease(from prereleaseElements: [String]) -> String? { + prereleaseElements.isEmpty ? nil : prereleaseElements.joined(separator: ".") + } + + static func build(from buildElements: [String]) -> String? { + buildElements.isEmpty ? nil : buildElements.joined(separator: ".") + } + + var core: String { + Self.core(from: coreElements) + } + + var prerelease: String? { + Self.prerelease(from: prereleaseElements) + } + + var build: String? { + Self.build(from: buildElements) + } +} diff --git a/Sources/mas/Utilities/Versions/SemVerSyntaxInteger.swift b/Sources/mas/Utilities/Versions/SemVerSyntaxInteger.swift new file mode 100644 index 000000000..eb9ce03ab --- /dev/null +++ b/Sources/mas/Utilities/Versions/SemVerSyntaxInteger.swift @@ -0,0 +1,8 @@ +// +// SemVerSyntaxInteger.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +protocol SemVerSyntaxInteger: CoreIntegerVersion, SemVerSyntax, MajorMinorPatchInteger {} diff --git a/Sources/mas/Utilities/Versions/UniversalSemVer.swift b/Sources/mas/Utilities/Versions/UniversalSemVer.swift new file mode 100644 index 000000000..e535be5f1 --- /dev/null +++ b/Sources/mas/Utilities/Versions/UniversalSemVer.swift @@ -0,0 +1,90 @@ +// +// UniversalSemVer.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +private import BigInt +internal import Foundation + +struct UniversalSemVer: SemVerSyntax, ExpressibleByStringLiteral { + let coreElements: [String] + let prereleaseElements: [String] + let buildElements: [String] + let rawValue: String + + init(rawValue: String) { + guard let match = rawValue.wholeMatch(of: universalSemVerRegex) else { + preconditionFailure("Failed to match regex \(universalSemVerRegex)") + } + + coreElements = match.1.elements + prereleaseElements = match.2.elements + buildElements = match.3.elements + self.rawValue = rawValue + } + + init(stringLiteral value: String) { + self.init(rawValue: value) + } +} + +private extension BigUInt { + func compare(to that: Self) -> ComparisonResult { + self < that ? .orderedAscending : self == that ? .orderedSame : .orderedDescending + } +} + +private extension FixedWidthInteger { + func compare(to that: Self) -> ComparisonResult { + self < that ? .orderedAscending : self == that ? .orderedSame : .orderedDescending + } +} + +private extension String { + func compareSemVerElement( + to that: Self, + options mask: CompareOptions = .init(), + range: Range? = nil, + locale: Locale? = nil, + ) -> ComparisonResult { + let thatInteger = BigUInt(that) + return BigUInt(self).map { thatInteger.map($0.compare(to:)) ?? .orderedAscending } + ?? thatInteger.map { _ in .orderedDescending } // swiftformat:disable:this indent + ?? compare(that, options: mask, range: range, locale: locale) // swiftformat:disable:this indent + } +} + +private extension [String] { + func compareSemVerElements(to that: Self) -> ComparisonResult { + zip(self, that).first { $0 != $1 }.map { $0.compareSemVerElement(to: $1) } + ?? dropLast { $0 == "0" }.count.compare(to: that.dropLast { $0 == "0" }.count) // swiftformat:disable:this indent + } +} + +extension Substring? { + var elements: [String] { + map { $0.split(separator: ".") }?.map(String.init(_:)) ?? .init() + } +} + +extension Version { + func compareSemVer(to that: Self) -> ComparisonResult { + let coreComparison = coreElements.compareSemVerElements(to: that.coreElements) + return coreComparison != .orderedSame + ? coreComparison // swiftformat:disable:this indent + : prereleaseElements.isEmpty != that.prereleaseElements.isEmpty // swiftformat:disable:next wrap wrapArguments + ? prereleaseElements.isEmpty ? .orderedDescending : .orderedAscending + : prereleaseElements.compareSemVerElements(to: that.prereleaseElements) + } + + func compareSemVerAndBuild(to that: Self) -> ComparisonResult { + let semVerComparison = compareSemVer(to: that) + return semVerComparison == .orderedSame + ? buildElements.compareSemVerElements(to: that.buildElements) // swiftformat:disable:this indent + : semVerComparison + } +} + +let universalSemVerRegex = /([^-+]*+)?+(?:-([^+]*+))?+(?:\+(.*+))?+/ diff --git a/Sources/mas/Utilities/Versions/UniversalSemVerInt.swift b/Sources/mas/Utilities/Versions/UniversalSemVerInt.swift new file mode 100644 index 000000000..f5debfb2d --- /dev/null +++ b/Sources/mas/Utilities/Versions/UniversalSemVerInt.swift @@ -0,0 +1,73 @@ +// +// UniversalSemVerInt.swift +// mas +// +// Copyright © 2026 mas-cli. All rights reserved. +// + +struct UniversalSemVerInt: SemVerSyntaxInteger { + let coreIntegers: [Int] + let prereleaseElements: [String] + let buildElements: [String] + let rawValue: String + + var majorInteger: Int { + coreIntegers[0] + } + + var minorInteger: Int { + coreIntegers[1] + } + + var patchInteger: Int { + coreIntegers[2] + } + + init( + coreIntegers: [Int], + prereleaseElements: [String] = .init(), + buildElements: [String] = .init(), + ) { // periphery:ignore + self.init( + coreIntegers: coreIntegers, + prereleaseElements: prereleaseElements, + buildElements: buildElements, + rawValue: """ + \(Self.core(from: Self.coreElements(from: coreIntegers)))\ + \(Self.prerelease(from: prereleaseElements).map { "-\($0)" } ?? "")\ + \(Self.build(from: buildElements).map { "+\($0)" } ?? "") + """, + ) + } + + init?(rawValue: String) { + do { + guard let match = rawValue.wholeMatch(of: universalSemVerRegex) else { + preconditionFailure("Failed to match regex \(universalSemVerRegex)") + } + + self = .init( + coreIntegers: try match.1.elements.map { coreElement in + try .init(coreElement) ?? { throw MASError.error(coreElement) }() + }, + prereleaseElements: match.2.elements, + buildElements: match.3.elements, + rawValue: rawValue, + ) + } catch { + return nil + } + } + + private init( + coreIntegers: [Int], + prereleaseElements: [String], + buildElements: [String], + rawValue: String, + ) { + self.coreIntegers = coreIntegers.padding(toCount: 3, with: 0) + self.prereleaseElements = prereleaseElements + self.buildElements = buildElements + self.rawValue = rawValue + } +} diff --git a/Sources/mas/Utilities/Versions/Version+SemVer.swift b/Sources/mas/Utilities/Versions/Version+SemVer.swift deleted file mode 100644 index eb63acd26..000000000 --- a/Sources/mas/Utilities/Versions/Version+SemVer.swift +++ /dev/null @@ -1,233 +0,0 @@ -// -// Version+SemVer.swift -// mas -// -// Copyright © 2025 mas-cli. All rights reserved. -// - -private import BigInt -internal import Foundation - -// swiftlint:disable:next blanket_disable_command -// swiftlint:disable file_types_order one_declaration_per_file - -protocol Version: RawRepresentable { - var coreElements: [String] { get } - var prereleaseElements: [String] { get } - var buildElements: [String] { get } - - // swiftlint:disable unused_declaration - var core: String { get } // periphery:ignore - var prerelease: String? { get } // periphery:ignore - var build: String? { get } // periphery:ignore -} // swiftlint:enable unused_declaration - -protocol CoreIntegerVersion: Version { - associatedtype Integer: BinaryInteger - - var coreIntegers: [Integer] { get } -} - -extension CoreIntegerVersion where Integer: FixedWidthInteger { - fileprivate static func coreElements(from coreIntegers: [Integer]) -> [String] { - coreIntegers.map { .init($0) } - } - - var coreElements: [String] { - Self.coreElements(from: coreIntegers) - } -} - -protocol MajorMinorPatch { // swiftlint:disable unused_declaration - var major: String { get } // periphery:ignore - var minor: String { get } // periphery:ignore - var patch: String { get } // periphery:ignore -} // swiftlint:enable unused_declaration - -protocol MajorMinorPatchInteger: MajorMinorPatch { - associatedtype Integer: BinaryInteger - - var majorInteger: Integer { get } - var minorInteger: Integer { get } - var patchInteger: Integer { get } -} - -extension MajorMinorPatchInteger { - var major: String { - .init(majorInteger) - } - - var minor: String { - .init(minorInteger) - } - - var patch: String { - .init(patchInteger) - } -} - -protocol SemVerSyntax: Version {} - -extension SemVerSyntax { - fileprivate static func core(from coreElements: [String]) -> String { - coreElements.joined(separator: ".") - } - - fileprivate static func prerelease(from prereleaseElements: [String]) -> String? { - prereleaseElements.isEmpty ? nil : prereleaseElements.joined(separator: ".") - } - - fileprivate static func build(from buildElements: [String]) -> String? { - buildElements.isEmpty ? nil : buildElements.joined(separator: ".") - } - - var core: String { - Self.core(from: coreElements) - } - - var prerelease: String? { - Self.prerelease(from: prereleaseElements) - } - - var build: String? { - Self.build(from: buildElements) - } -} - -protocol SemVerSyntaxInteger: CoreIntegerVersion, SemVerSyntax, MajorMinorPatchInteger {} - -struct UniversalSemVerInt: SemVerSyntaxInteger { - let coreIntegers: [Int] - let prereleaseElements: [String] - let buildElements: [String] - let rawValue: String - - var majorInteger: Int { - coreIntegers[0] - } - - var minorInteger: Int { - coreIntegers[1] - } - - var patchInteger: Int { - coreIntegers[2] - } - - init( - coreIntegers: [Int], - prereleaseElements: [String] = .init(), - buildElements: [String] = .init(), - ) { // periphery:ignore - self.init( - coreIntegers: coreIntegers, - prereleaseElements: prereleaseElements, - buildElements: buildElements, - rawValue: """ - \(Self.core(from: Self.coreElements(from: coreIntegers)))\ - \(Self.prerelease(from: prereleaseElements).map { "-\($0)" } ?? "")\ - \(Self.build(from: buildElements).map { "+\($0)" } ?? "") - """, - ) - } - - init?(rawValue: String) { - do { - let match = rawValue.wholeMatch(of: universalSemVerRegex)! // swiftlint:disable:this force_unwrapping - self = .init( - coreIntegers: try match.1.elements.map { coreElement in - try .init(coreElement) ?? { throw MASError.error(coreElement) }() - }, - prereleaseElements: match.2.elements, - buildElements: match.3.elements, - rawValue: rawValue, - ) - } catch { - return nil - } - } - - private init( - coreIntegers: [Int], - prereleaseElements: [String], - buildElements: [String], - rawValue: String, - ) { - self.coreIntegers = coreIntegers.padding(toCount: 3, with: 0) - self.prereleaseElements = prereleaseElements - self.buildElements = buildElements - self.rawValue = rawValue - } -} - -struct UniversalSemVer: SemVerSyntax { - let coreElements: [String] - let prereleaseElements: [String] - let buildElements: [String] - let rawValue: String - - init(rawValue: String) { - let match = rawValue.wholeMatch(of: universalSemVerRegex)! // swiftlint:disable:this force_unwrapping - coreElements = match.1.elements - prereleaseElements = match.2.elements - buildElements = match.3.elements - self.rawValue = rawValue - } -} - -private extension BigUInt { - func compare(to that: Self) -> ComparisonResult { - self < that ? .orderedAscending : self == that ? .orderedSame : .orderedDescending - } -} - -private extension FixedWidthInteger { - func compare(to that: Self) -> ComparisonResult { - self < that ? .orderedAscending : self == that ? .orderedSame : .orderedDescending - } -} - -private extension String { - func compareSemVerElement( - to that: Self, - options mask: CompareOptions = .init(), - range: Range? = nil, - locale: Locale? = nil, - ) -> ComparisonResult { - let thatInteger = BigUInt(that) - return BigUInt(self).map { thatInteger.map($0.compare(to:)) ?? .orderedAscending } - ?? thatInteger.map { _ in .orderedDescending } // swiftformat:disable:this indent - ?? compare(that, options: mask, range: range, locale: locale) // swiftformat:disable:this indent - } -} - -private extension [String] { - func compareSemVerElements(to that: Self) -> ComparisonResult { - zip(self, that).first { $0 != $1 }.map { $0.compareSemVerElement(to: $1) } - ?? dropLast { $0 == "0" }.count.compare(to: that.dropLast { $0 == "0" }.count) // swiftformat:disable:this indent - } -} - -private extension Substring? { - var elements: [String] { - map { $0.split(separator: ".") }?.map(String.init(_:)) ?? .init() - } -} - -extension Version { - func compareSemVer(to that: Self) -> ComparisonResult { - let coreComparison = coreElements.compareSemVerElements(to: that.coreElements) - return coreComparison == .orderedSame - ? prereleaseElements.compareSemVerElements(to: that.prereleaseElements) // swiftformat:disable:this indent - : coreComparison - } - - func compareSemVerAndBuild(to that: Self) -> ComparisonResult { - let semVerComparison = compareSemVer(to: that) - return semVerComparison == .orderedSame - ? buildElements.compareSemVerElements(to: that.buildElements) // swiftformat:disable:this indent - : semVerComparison - } -} - -private let universalSemVerRegex = /([^-+]*+)?+(?:-([^+]*+))?+(?:\+(.*+))?+/ diff --git a/Sources/mas/Utilities/Versions/Version.swift b/Sources/mas/Utilities/Versions/Version.swift new file mode 100644 index 000000000..26355867e --- /dev/null +++ b/Sources/mas/Utilities/Versions/Version.swift @@ -0,0 +1,16 @@ +// +// Version.swift +// mas +// +// Copyright © 2025 mas-cli. All rights reserved. +// + +protocol Version: RawRepresentable { + var coreElements: [String] { get } + var prereleaseElements: [String] { get } + var buildElements: [String] { get } // swiftlint:disable unused_declaration + + var core: String { get } // periphery:ignore + var prerelease: String? { get } // periphery:ignore + var build: String? { get } // periphery:ignore +} // swiftlint:enable unused_declaration diff --git a/Tests/MASTests/Commands/MASTests+Home.swift b/Tests/MASTests/Commands/MASTests+MAS.Home.swift similarity index 94% rename from Tests/MASTests/Commands/MASTests+Home.swift rename to Tests/MASTests/Commands/MASTests+MAS.Home.swift index 63e8c4da2..28647060e 100644 --- a/Tests/MASTests/Commands/MASTests+Home.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Home.swift @@ -1,5 +1,5 @@ // -// MASTests+Home.swift +// MASTests+MAS.Home.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+List.swift b/Tests/MASTests/Commands/MASTests+MAS.List.swift similarity index 97% rename from Tests/MASTests/Commands/MASTests+List.swift rename to Tests/MASTests/Commands/MASTests+MAS.List.swift index 52a8a1e21..7408294f6 100644 --- a/Tests/MASTests/Commands/MASTests+List.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.List.swift @@ -1,5 +1,5 @@ // -// MASTests+List.swift +// MASTests+MAS.List.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+Lookup.swift b/Tests/MASTests/Commands/MASTests+MAS.Lookup.swift similarity index 99% rename from Tests/MASTests/Commands/MASTests+Lookup.swift rename to Tests/MASTests/Commands/MASTests+MAS.Lookup.swift index fc6f806ae..2a29b06d3 100644 --- a/Tests/MASTests/Commands/MASTests+Lookup.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Lookup.swift @@ -1,5 +1,5 @@ // -// MASTests+Lookup.swift +// MASTests+MAS.Lookup.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+Search.swift b/Tests/MASTests/Commands/MASTests+MAS.Search.swift similarity index 99% rename from Tests/MASTests/Commands/MASTests+Search.swift rename to Tests/MASTests/Commands/MASTests+MAS.Search.swift index 04cfca448..b226630a7 100644 --- a/Tests/MASTests/Commands/MASTests+Search.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Search.swift @@ -1,5 +1,5 @@ // -// MASTests+Search.swift +// MASTests+MAS.Search.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+Seller.swift b/Tests/MASTests/Commands/MASTests+MAS.Seller.swift similarity index 93% rename from Tests/MASTests/Commands/MASTests+Seller.swift rename to Tests/MASTests/Commands/MASTests+MAS.Seller.swift index 802b26906..1e450191e 100644 --- a/Tests/MASTests/Commands/MASTests+Seller.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Seller.swift @@ -1,5 +1,5 @@ // -// MASTests+Seller.swift +// MASTests+MAS.Seller.swift // mas // // Copyright © 2019 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Commands/MASTests+Version.swift b/Tests/MASTests/Commands/MASTests+MAS.Version.swift similarity index 92% rename from Tests/MASTests/Commands/MASTests+Version.swift rename to Tests/MASTests/Commands/MASTests+MAS.Version.swift index 8dc37a65a..6d873dbfb 100644 --- a/Tests/MASTests/Commands/MASTests+Version.swift +++ b/Tests/MASTests/Commands/MASTests+MAS.Version.swift @@ -1,5 +1,5 @@ // -// MASTests+Version.swift +// MASTests+MAS.Version.swift // mas // // Copyright © 2018 mas-cli. All rights reserved. diff --git a/Tests/MASTests/Utilities/Consequences.swift b/Tests/MASTests/Testing/Consequences.swift similarity index 100% rename from Tests/MASTests/Utilities/Consequences.swift rename to Tests/MASTests/Testing/Consequences.swift diff --git a/Tests/MASTests/Utilities/Data.swift b/Tests/MASTests/Testing/Data.swift similarity index 100% rename from Tests/MASTests/Utilities/Data.swift rename to Tests/MASTests/Testing/Data.swift diff --git a/Tests/MASTests/Utilities/Resources.swift b/Tests/MASTests/Testing/Resources.swift similarity index 100% rename from Tests/MASTests/Utilities/Resources.swift rename to Tests/MASTests/Testing/Resources.swift