mirror of
https://github.com/mas-cli/mas
synced 2024-11-22 19:43:09 +00:00
Merge pull request #198 from mas-cli/network-refactor
♻️🌐 Network refactor
This commit is contained in:
commit
893c528a2f
83 changed files with 1653 additions and 745 deletions
9
.hound.yml
Normal file
9
.hound.yml
Normal file
|
@ -0,0 +1,9 @@
|
|||
#
|
||||
# .hound.yml
|
||||
# mas
|
||||
#
|
||||
# http://help.houndci.com/configuration/swiftlint
|
||||
#
|
||||
---
|
||||
swiftlint:
|
||||
config_file: .swiftlint.yml
|
11
.swiftlint.yml
Normal file
11
.swiftlint.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
#
|
||||
# .swiftlint.yml
|
||||
# mas
|
||||
#
|
||||
# https://github.com/realm/SwiftLint#configuration
|
||||
#
|
||||
---
|
||||
excluded:
|
||||
- Carthage/
|
||||
- docs/
|
||||
- MasKitTests/
|
|
@ -7,10 +7,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
- ♻️🌐 Network refactor #198
|
||||
- 👷🏻♀️ Jenkins Pipeline #197
|
||||
- ✨ New `home`, `open` and `vendor` commands #196
|
||||
- 🐛📦 Fix paths building installer package #195
|
||||
- ♻️ AppLibrary refactor #193
|
||||
- ♻️📚 AppLibrary refactor #193
|
||||
|
||||
## [v1.5.0] 🗑 Uninstall - 2018-12-27
|
||||
|
||||
|
|
8
Carthage/Checkouts/Commandant/README.md
vendored
8
Carthage/Checkouts/Commandant/README.md
vendored
|
@ -28,11 +28,11 @@ struct LogOptions: OptionsProtocol {
|
|||
return { verbose in { logName in LogOptions(lines: lines, verbose: verbose, logName: logName) } }
|
||||
}
|
||||
|
||||
static func evaluate(_ m: CommandMode) -> Result<LogOptions, CommandantError<YourErrorType>> {
|
||||
static func evaluate(_ mode: CommandMode) -> Result<LogOptions, CommandantError<YourErrorType>> {
|
||||
return create
|
||||
<*> m <| Option(key: "lines", defaultValue: 0, usage: "the number of lines to read from the logs")
|
||||
<*> m <| Option(key: "verbose", defaultValue: false, usage: "show verbose output")
|
||||
<*> m <| Argument(usage: "the log to read")
|
||||
<*> mode <| Option(key: "lines", defaultValue: 0, usage: "the number of lines to read from the logs")
|
||||
<*> mode <| Option(key: "verbose", defaultValue: false, usage: "show verbose output")
|
||||
<*> mode <| Argument(usage: "the log to read")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -69,8 +69,8 @@ public struct HelpOptions<ClientError: Error>: OptionsProtocol {
|
|||
return self.init(verb: (verb == "" ? nil : verb))
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<HelpOptions, CommandantError<ClientError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<HelpOptions, CommandantError<ClientError>> {
|
||||
return create
|
||||
<*> m <| Argument(defaultValue: "", usage: "the command to display help for")
|
||||
<*> mode <| Argument(defaultValue: "", usage: "the command to display help for")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,12 +27,12 @@ import Result
|
|||
/// return { outputFilename in { shouldDelete in { logName in LogOptions(verbosity: verbosity, outputFilename: outputFilename, shouldDelete: shouldDelete, logName: logName) } } }
|
||||
/// }
|
||||
///
|
||||
/// static func evaluate(_ m: CommandMode) -> Result<LogOptions, CommandantError<YourErrorType>> {
|
||||
/// static func evaluate(_ mode: CommandMode) -> Result<LogOptions, CommandantError<YourErrorType>> {
|
||||
/// return create
|
||||
/// <*> m <| Option(key: "verbose", defaultValue: 0, usage: "the verbosity level with which to read the logs")
|
||||
/// <*> m <| Option(key: "outputFilename", defaultValue: "", usage: "a file to print output to, instead of stdout")
|
||||
/// <*> m <| Switch(flag: "d", key: "delete", usage: "delete the logs when finished")
|
||||
/// <*> m <| Argument(usage: "the log to read")
|
||||
/// <*> mode <| Option(key: "verbose", defaultValue: 0, usage: "the verbosity level with which to read the logs")
|
||||
/// <*> mode <| Option(key: "outputFilename", defaultValue: "", usage: "a file to print output to, instead of stdout")
|
||||
/// <*> mode <| Switch(flag: "d", key: "delete", usage: "delete the logs when finished")
|
||||
/// <*> mode <| Argument(usage: "the log to read")
|
||||
/// }
|
||||
/// }
|
||||
public protocol OptionsProtocol {
|
||||
|
@ -41,14 +41,14 @@ public protocol OptionsProtocol {
|
|||
/// Evaluates this set of options in the given mode.
|
||||
///
|
||||
/// Returns the parsed options or a `UsageError`.
|
||||
static func evaluate(_ m: CommandMode) -> Result<Self, CommandantError<ClientError>>
|
||||
static func evaluate(_ mode: CommandMode) -> Result<Self, CommandantError<ClientError>>
|
||||
}
|
||||
|
||||
/// An `OptionsProtocol` that has no options.
|
||||
public struct NoOptions<ClientError: Error>: OptionsProtocol {
|
||||
public init() {}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<NoOptions, CommandantError<ClientError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<NoOptions, CommandantError<ClientError>> {
|
||||
return .success(NoOptions())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -157,19 +157,19 @@ struct TestOptions: OptionsProtocol, Equatable {
|
|||
} } } } } } } } } }
|
||||
}
|
||||
|
||||
static func evaluate(_ m: CommandMode) -> Result<TestOptions, CommandantError<NoError>> {
|
||||
static func evaluate(_ mode: CommandMode) -> Result<TestOptions, CommandantError<NoError>> {
|
||||
return create
|
||||
<*> m <| Option(key: "intValue", defaultValue: 42, usage: "Some integer value")
|
||||
<*> m <| Option(key: "stringValue", defaultValue: "foobar", usage: "Some string value")
|
||||
<*> m <| Option<[String]>(key: "stringsArray", defaultValue: [], usage: "Some array of arguments")
|
||||
<*> m <| Option<[String]?>(key: "optionalStringsArray", defaultValue: nil, usage: "Some array of arguments")
|
||||
<*> m <| Option<String?>(key: "optionalStringValue", defaultValue: nil, usage: "Some string value")
|
||||
<*> m <| Argument(usage: "A name you're required to specify")
|
||||
<*> m <| Argument(defaultValue: "filename", usage: "A filename that you can optionally specify")
|
||||
<*> m <| Option(key: "enabled", defaultValue: false, usage: "Whether to be enabled")
|
||||
<*> m <| Switch(flag: "f", key: "force", usage: "Whether to force")
|
||||
<*> m <| Switch(flag: "g", key: "glob", usage: "Whether to glob")
|
||||
<*> m <| Argument(defaultValue: [], usage: "An argument list that consumes the rest of positional arguments")
|
||||
<*> mode <| Option(key: "intValue", defaultValue: 42, usage: "Some integer value")
|
||||
<*> mode <| Option(key: "stringValue", defaultValue: "foobar", usage: "Some string value")
|
||||
<*> mode <| Option<[String]>(key: "stringsArray", defaultValue: [], usage: "Some array of arguments")
|
||||
<*> mode <| Option<[String]?>(key: "optionalStringsArray", defaultValue: nil, usage: "Some array of arguments")
|
||||
<*> mode <| Option<String?>(key: "optionalStringValue", defaultValue: nil, usage: "Some string value")
|
||||
<*> mode <| Argument(usage: "A name you're required to specify")
|
||||
<*> mode <| Argument(defaultValue: "filename", usage: "A filename that you can optionally specify")
|
||||
<*> mode <| Option(key: "enabled", defaultValue: false, usage: "Whether to be enabled")
|
||||
<*> mode <| Switch(flag: "f", key: "force", usage: "Whether to force")
|
||||
<*> mode <| Switch(flag: "g", key: "glob", usage: "Whether to glob")
|
||||
<*> mode <| Argument(defaultValue: [], usage: "An argument list that consumes the rest of positional arguments")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -132,16 +132,16 @@ struct TestEnumOptions: OptionsProtocol, Equatable {
|
|||
} } } } } } }
|
||||
}
|
||||
|
||||
static func evaluate(_ m: CommandMode) -> Result<TestEnumOptions, CommandantError<NoError>> {
|
||||
static func evaluate(_ mode: CommandMode) -> Result<TestEnumOptions, CommandantError<NoError>> {
|
||||
return create
|
||||
<*> m <| Option(key: "strictIntValue", defaultValue: .theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything, usage: "`0` - zero, `255` - max, `3` - three, `5` - five or `42` - The Answer")
|
||||
<*> m <| Option(key: "strictStringValue", defaultValue: .foobar, usage: "`foobar`, `bazbuzzz`, `a`, `b`, `c`, `one`, `two`, `c`")
|
||||
<*> m <| Option<[StrictStringValue]>(key: "strictStringsArray", defaultValue: [], usage: "Some array of arguments")
|
||||
<*> m <| Option<[StrictStringValue]?>(key: "optionalStrictStringsArray", defaultValue: nil, usage: "Some array of arguments")
|
||||
<*> m <| Option<StrictStringValue?>(key: "optionalStrictStringValue", defaultValue: nil, usage: "Some string value")
|
||||
<*> m <| Argument(usage: "A name you're required to specify")
|
||||
<*> m <| Argument(defaultValue: .min, usage: "A number that you can optionally specify")
|
||||
<*> m <| Argument(defaultValue: [], usage: "An argument list that consumes the rest of positional arguments")
|
||||
<*> mode <| Option(key: "strictIntValue", defaultValue: .theAnswerToTheUltimateQuestionOfLifeTheUniverseAndEverything, usage: "`0` - zero, `255` - max, `3` - three, `5` - five or `42` - The Answer")
|
||||
<*> mode <| Option(key: "strictStringValue", defaultValue: .foobar, usage: "`foobar`, `bazbuzzz`, `a`, `b`, `c`, `one`, `two`, `c`")
|
||||
<*> mode <| Option<[StrictStringValue]>(key: "strictStringsArray", defaultValue: [], usage: "Some array of arguments")
|
||||
<*> mode <| Option<[StrictStringValue]?>(key: "optionalStrictStringsArray", defaultValue: nil, usage: "Some array of arguments")
|
||||
<*> mode <| Option<StrictStringValue?>(key: "optionalStrictStringValue", defaultValue: nil, usage: "Some string value")
|
||||
<*> mode <| Argument(usage: "A name you're required to specify")
|
||||
<*> mode <| Argument(defaultValue: .min, usage: "A number that you can optionally specify")
|
||||
<*> mode <| Argument(defaultValue: [], usage: "An argument list that consumes the rest of positional arguments")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,18 +9,23 @@
|
|||
import CommerceKit
|
||||
import StoreFoundation
|
||||
|
||||
/// Monitors app download progress.
|
||||
///
|
||||
/// - Parameter adamId: An app ID?
|
||||
/// - Returns: An error, if one occurred.
|
||||
func download(_ adamId: UInt64) -> MASError? {
|
||||
|
||||
guard let account = ISStoreAccount.primaryAccount else {
|
||||
return .notSignedIn
|
||||
}
|
||||
|
||||
let group = DispatchGroup()
|
||||
let purchase = SSPurchase(adamId: adamId, account: account as! ISStoreAccount)
|
||||
|
||||
|
||||
guard let storeAccount = account as? ISStoreAccount
|
||||
else { fatalError("Unable to cast StoreAccount to ISStoreAccount") }
|
||||
let purchase = SSPurchase(adamId: adamId, account: storeAccount)
|
||||
|
||||
var purchaseError: MASError?
|
||||
var observerIdentifier: CKDownloadQueueObserver? = nil
|
||||
|
||||
var observerIdentifier: CKDownloadQueueObserver?
|
||||
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
purchase.perform { purchase, _, error, response in
|
||||
if let error = error {
|
||||
|
@ -28,7 +33,7 @@ func download(_ adamId: UInt64) -> MASError? {
|
|||
group.leave()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let downloads = response?.downloads, downloads.count > 0, let purchase = purchase {
|
||||
let observer = PurchaseDownloadObserver(purchase: purchase)
|
||||
|
||||
|
@ -36,27 +41,25 @@ func download(_ adamId: UInt64) -> MASError? {
|
|||
purchaseError = error
|
||||
group.leave()
|
||||
}
|
||||
|
||||
|
||||
observer.completionHandler = {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
let downloadQueue = CKDownloadQueue.shared()
|
||||
observerIdentifier = downloadQueue.add(observer)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
print("No downloads")
|
||||
purchaseError = .noDownloads
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
let _ = group.wait(timeout: .distantFuture)
|
||||
|
||||
|
||||
_ = group.wait(timeout: .distantFuture)
|
||||
|
||||
if let observerIdentifier = observerIdentifier {
|
||||
CKDownloadQueue.shared().remove(observerIdentifier)
|
||||
}
|
||||
|
||||
return purchaseError
|
||||
}
|
||||
|
||||
|
|
|
@ -38,8 +38,8 @@ extension ISStoreAccount: StoreAccount {
|
|||
}
|
||||
|
||||
static func signIn(username: String, password: String, systemDialog: Bool = false) throws -> StoreAccount {
|
||||
var account: ISStoreAccount? = nil
|
||||
var error: MASError? = nil
|
||||
var storeAccount: ISStoreAccount?
|
||||
var maserror: MASError?
|
||||
|
||||
let accountService: ISAccountService = ISServiceProxy.genericShared().accountService
|
||||
let client = ISStoreClient(storeClientType: 0)
|
||||
|
@ -61,11 +61,11 @@ extension ISStoreAccount: StoreAccount {
|
|||
group.enter()
|
||||
|
||||
// Only works on macOS Sierra and below
|
||||
accountService.signIn(with: context) { success, _account, _error in
|
||||
accountService.signIn(with: context) { success, account, error in
|
||||
if success {
|
||||
account = _account
|
||||
storeAccount = account
|
||||
} else {
|
||||
error = .signInFailed(error: _error as NSError?)
|
||||
maserror = .signInFailed(error: error as NSError?)
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
|
@ -73,13 +73,13 @@ extension ISStoreAccount: StoreAccount {
|
|||
if systemDialog {
|
||||
group.wait()
|
||||
} else {
|
||||
let _ = group.wait(timeout: .now() + 30)
|
||||
_ = group.wait(timeout: .now() + 30)
|
||||
}
|
||||
|
||||
if let account = account {
|
||||
|
||||
if let account = storeAccount {
|
||||
return account
|
||||
}
|
||||
|
||||
throw error ?? MASError.signInFailed(error: nil)
|
||||
|
||||
throw maserror ?? MASError.signInFailed(error: nil)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,27 +11,26 @@ import StoreFoundation
|
|||
|
||||
@objc class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
|
||||
let purchase: SSPurchase
|
||||
var completionHandler: (() -> ())?
|
||||
var errorHandler: ((MASError) -> ())?
|
||||
|
||||
var completionHandler: (() -> Void)?
|
||||
var errorHandler: ((MASError) -> Void)?
|
||||
|
||||
init(purchase: SSPurchase) {
|
||||
self.purchase = purchase
|
||||
}
|
||||
|
||||
|
||||
func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) {
|
||||
guard download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
let status = download.status else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if status.isFailed || status.isCancelled {
|
||||
queue.removeDownload(withItemIdentifier: download.metadata.itemIdentifier)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
progress(status.progressState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func downloadQueue(_ queue: CKDownloadQueue, changedWithAddition download: SSDownload) {
|
||||
guard download.metadata.itemIdentifier == purchase.itemIdentifier else {
|
||||
return
|
||||
|
@ -39,21 +38,19 @@ import StoreFoundation
|
|||
clearLine()
|
||||
printInfo("Downloading \(download.metadata.title)")
|
||||
}
|
||||
|
||||
|
||||
func downloadQueue(_ queue: CKDownloadQueue, changedWithRemoval download: SSDownload) {
|
||||
guard download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
let status = download.status else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
clearLine()
|
||||
if status.isFailed {
|
||||
errorHandler?(.downloadFailed(error: status.error as NSError?))
|
||||
}
|
||||
else if status.isCancelled {
|
||||
} else if status.isCancelled {
|
||||
errorHandler?(.cancelled)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
printInfo("Installed \(download.metadata.title)")
|
||||
completionHandler?()
|
||||
}
|
||||
|
@ -63,7 +60,7 @@ import StoreFoundation
|
|||
struct ProgressState {
|
||||
let percentComplete: Float
|
||||
let phase: String
|
||||
|
||||
|
||||
var percentage: String {
|
||||
return String(format: "%.1f%%", arguments: [floor(percentComplete * 100)])
|
||||
}
|
||||
|
@ -74,16 +71,15 @@ func progress(_ state: ProgressState) {
|
|||
guard isatty(fileno(stdout)) != 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let barLength = 60
|
||||
|
||||
|
||||
let completeLength = Int(state.percentComplete * Float(barLength))
|
||||
var bar = ""
|
||||
for i in 0..<barLength {
|
||||
if i < completeLength {
|
||||
for index in 0..<barLength {
|
||||
if index < completeLength {
|
||||
bar += "#"
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
bar += "-"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,23 +9,25 @@
|
|||
import CommerceKit
|
||||
import StoreFoundation
|
||||
|
||||
typealias SSPurchaseCompletion = (_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> ()
|
||||
typealias SSPurchaseCompletion =
|
||||
(_ purchase: SSPurchase?, _ completed: Bool, _ error: Error?, _ response: SSPurchaseResponse?) -> Void
|
||||
|
||||
extension SSPurchase {
|
||||
convenience init(adamId: UInt64, account: ISStoreAccount) {
|
||||
self.init()
|
||||
self.buyParameters = "productType=C&price=0&salableAdamId=\(adamId)&pricingParameters=STDRDL&pg=default&appExtVrsId=0"
|
||||
self.buyParameters =
|
||||
"productType=C&price=0&salableAdamId=\(adamId)&pricingParameters=STDRDL&pg=default&appExtVrsId=0"
|
||||
self.itemIdentifier = adamId
|
||||
self.accountIdentifier = account.dsID
|
||||
self.appleID = account.identifier
|
||||
|
||||
|
||||
let downloadMetadata = SSDownloadMetadata()
|
||||
downloadMetadata.kind = "software"
|
||||
downloadMetadata.itemIdentifier = adamId
|
||||
|
||||
|
||||
self.downloadMetadata = downloadMetadata
|
||||
}
|
||||
|
||||
|
||||
func perform(_ completion: @escaping SSPurchaseCompletion) {
|
||||
CKPurchaseController.shared().perform(self, withOptions: 0, completionHandler: completion)
|
||||
}
|
||||
|
|
|
@ -16,12 +16,12 @@ public struct AccountCommand: CommandProtocol {
|
|||
public let function = "Prints the primary account Apple ID"
|
||||
|
||||
public init() {}
|
||||
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
if let account = ISStoreAccount.primaryAccount {
|
||||
print(String(describing: account.identifier))
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
print("Not signed in")
|
||||
return .failure(.notSignedIn)
|
||||
}
|
||||
|
|
|
@ -47,8 +47,7 @@ public struct HomeCommand: CommandProtocol {
|
|||
printError("Open failed: (\(reason)) \(openCommand.stderr)")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
|
@ -67,8 +66,8 @@ public struct HomeOptions: OptionsProtocol {
|
|||
return HomeOptions(appId: appId)
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<HomeOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<HomeOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(usage: "ID of app to show on MAS Preview")
|
||||
<*> mode <| Argument(usage: "ID of app to show on MAS Preview")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,36 +16,33 @@ public struct InfoCommand: CommandProtocol {
|
|||
public let verb = "info"
|
||||
public let function = "Display app information from the Mac App Store"
|
||||
|
||||
private let urlSession: URLSession
|
||||
|
||||
public init(urlSession: URLSession = URLSession.shared) {
|
||||
self.urlSession = urlSession
|
||||
private let storeSearch: StoreSearch
|
||||
|
||||
/// Designated initializer.
|
||||
public init(storeSearch: StoreSearch = MasStoreSearch()) {
|
||||
self.storeSearch = storeSearch
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: InfoOptions) -> Result<(), MASError> {
|
||||
guard let infoURLString = infoURLString(options.appId),
|
||||
let searchJson = urlSession.requestSynchronousJSONWithURLString(infoURLString) as? [String: Any] else {
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(app: options.appId)
|
||||
else {
|
||||
print("No results found")
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
guard let resultCount = searchJson[ResultKeys.ResultCount] as? Int, resultCount > 0,
|
||||
let results = searchJson[ResultKeys.Results] as? [[String: Any]],
|
||||
let result = results.first else {
|
||||
print("No results found")
|
||||
return .failure(.noSearchResultsFound)
|
||||
print(AppInfoFormatter.format(app: result))
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
print(AppInfoFormatter.format(appInfo: result))
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
private func infoURLString(_ appId: String) -> String? {
|
||||
if let urlEncodedAppId = appId.URLEncodedString {
|
||||
return "https://itunes.apple.com/lookup?id=\(urlEncodedAppId)"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public struct InfoOptions: OptionsProtocol {
|
||||
|
@ -55,60 +52,8 @@ public struct InfoOptions: OptionsProtocol {
|
|||
return InfoOptions(appId: appId)
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<InfoOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<InfoOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(usage: "the app id to show info")
|
||||
}
|
||||
}
|
||||
|
||||
private struct AppInfoFormatter {
|
||||
private enum Keys {
|
||||
static let Name = "trackCensoredName"
|
||||
static let Version = "version"
|
||||
static let Price = "formattedPrice"
|
||||
static let Seller = "sellerName"
|
||||
static let VersionReleaseDate = "currentVersionReleaseDate"
|
||||
static let MinimumOS = "minimumOsVersion"
|
||||
static let FileSize = "fileSizeBytes"
|
||||
static let AppStoreUrl = "trackViewUrl"
|
||||
}
|
||||
|
||||
static func format(appInfo: [String: Any]) -> String {
|
||||
let headline = [
|
||||
"\(appInfo.stringOrEmpty(key: Keys.Name))",
|
||||
"\(appInfo.stringOrEmpty(key: Keys.Version))",
|
||||
"[\(appInfo.stringOrEmpty(key: Keys.Price))]",
|
||||
].joined(separator: " ")
|
||||
|
||||
return [
|
||||
headline,
|
||||
"By: \(appInfo.stringOrEmpty(key: Keys.Seller))",
|
||||
"Released: \(humaReadableDate(appInfo.stringOrEmpty(key: Keys.VersionReleaseDate)))",
|
||||
"Minimum OS: \(appInfo.stringOrEmpty(key: Keys.MinimumOS))",
|
||||
"Size: \(humanReadableSize(appInfo.stringOrEmpty(key: Keys.FileSize)))",
|
||||
"From: \(appInfo.stringOrEmpty(key: Keys.AppStoreUrl))",
|
||||
].joined(separator: "\n")
|
||||
}
|
||||
|
||||
private static func humanReadableSize(_ size: String) -> String {
|
||||
let bytesSize = Int64(size) ?? 0
|
||||
return ByteCountFormatter.string(fromByteCount: bytesSize, countStyle: .file)
|
||||
}
|
||||
|
||||
private static func humaReadableDate(_ serverDate: String) -> String {
|
||||
let serverDateFormatter = DateFormatter()
|
||||
serverDateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
serverDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
|
||||
|
||||
let humanDateFormatter = DateFormatter()
|
||||
humanDateFormatter.timeStyle = .none
|
||||
humanDateFormatter.dateStyle = .medium
|
||||
return serverDateFormatter.date(from: serverDate).flatMap(humanDateFormatter.string(from:)) ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
extension Dictionary {
|
||||
fileprivate func stringOrEmpty(key: Key) -> String {
|
||||
return self[key] as? String ?? ""
|
||||
<*> mode <| Argument(usage: "the app id to show info")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ public struct InstallCommand: CommandProtocol {
|
|||
self.appLibrary = appLibrary
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let downloadResults = options.appIds.compactMap { (appId) -> MASError? in
|
||||
|
@ -53,13 +54,13 @@ public struct InstallOptions: OptionsProtocol {
|
|||
|
||||
public static func create(_ appIds: [Int]) -> (_ forceInstall: Bool) -> InstallOptions {
|
||||
return { forceInstall in
|
||||
return InstallOptions(appIds: appIds.map{UInt64($0)}, forceInstall: forceInstall)
|
||||
return InstallOptions(appIds: appIds.map {UInt64($0)}, forceInstall: forceInstall)
|
||||
}
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<InstallOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<InstallOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(usage: "app ID(s) to install")
|
||||
<*> m <| Switch(flag: nil, key: "force", usage: "force reinstall")
|
||||
<*> mode <| Argument(usage: "app ID(s) to install")
|
||||
<*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ public struct ListCommand: CommandProtocol {
|
|||
self.appLibrary = appLibrary
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
let products = appLibrary.installedApps
|
||||
if products.isEmpty {
|
||||
|
|
|
@ -18,34 +18,49 @@ public struct LuckyCommand: CommandProtocol {
|
|||
public let function = "Install the first result from the Mac App Store"
|
||||
|
||||
private let appLibrary: AppLibrary
|
||||
private let urlSession: URLSession
|
||||
private let storeSearch: StoreSearch
|
||||
|
||||
/// Designated initializer.
|
||||
///
|
||||
/// - Parameter appLibrary: AppLibrary manager.
|
||||
/// - Parameter urlSession: URL session for network communication.
|
||||
public init(appLibrary: AppLibrary = MasAppLibrary(), urlSession: URLSession = URLSession.shared) {
|
||||
/// - Parameter storeSearch: Search manager.
|
||||
public init(appLibrary: AppLibrary = MasAppLibrary(),
|
||||
storeSearch: StoreSearch = MasStoreSearch()) {
|
||||
self.appLibrary = appLibrary
|
||||
self.urlSession = urlSession
|
||||
self.storeSearch = storeSearch
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
guard let searchURLString = searchURLString(options.appName),
|
||||
let searchJson = urlSession.requestSynchronousJSONWithURLString(searchURLString) as? [String: Any] else {
|
||||
var appId: Int?
|
||||
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(app: options.appName)
|
||||
else {
|
||||
print("No results found")
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
appId = result.trackId
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
guard let resultCount = searchJson[ResultKeys.ResultCount] as? Int, resultCount > 0,
|
||||
let results = searchJson[ResultKeys.Results] as? [[String: Any]] else {
|
||||
print("No results found")
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
guard let identifier = appId else { fatalError() }
|
||||
|
||||
let appId = results[0][ResultKeys.TrackId] as! UInt64
|
||||
|
||||
return install(appId, options: options)
|
||||
return install(UInt64(identifier), options: options)
|
||||
}
|
||||
|
||||
/// Installs an app.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - appId: App identifier
|
||||
/// - options: command opetions.
|
||||
/// - Returns: Result of the operation.
|
||||
fileprivate func install(_ appId: UInt64, options: Options) -> Result<(), MASError> {
|
||||
// Try to download applications with given identifiers and collect results
|
||||
let downloadResults = [appId].compactMap { (appId) -> MASError? in
|
||||
|
@ -66,13 +81,6 @@ public struct LuckyCommand: CommandProtocol {
|
|||
return .failure(.downloadFailed(error: nil))
|
||||
}
|
||||
}
|
||||
|
||||
func searchURLString(_ appName: String) -> String? {
|
||||
if let urlEncodedAppName = appName.URLEncodedString {
|
||||
return "https://itunes.apple.com/search?entity=macSoftware&term=\(urlEncodedAppName)&attribute=allTrackTerm"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public struct LuckyOptions: OptionsProtocol {
|
||||
|
@ -85,9 +93,9 @@ public struct LuckyOptions: OptionsProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<LuckyOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<LuckyOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(usage: "the app name to install")
|
||||
<*> m <| Switch(flag: nil, key: "force", usage: "force reinstall")
|
||||
<*> mode <| Argument(usage: "the app name to install")
|
||||
<*> mode <| Switch(flag: nil, key: "force", usage: "force reinstall")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,8 +53,7 @@ public struct OpenCommand: CommandProtocol {
|
|||
printError("Open failed: (\(reason)) \(systemOpen.stderr)")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
|
@ -73,8 +72,8 @@ public struct OpenOptions: OptionsProtocol {
|
|||
return OpenOptions(appId: appId)
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<OpenOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<OpenOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(usage: "the app id")
|
||||
<*> mode <| Argument(usage: "the app id")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,13 +26,16 @@ public struct OutdatedCommand: CommandProtocol {
|
|||
self.appLibrary = appLibrary
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
let updateController = CKUpdateController.shared()
|
||||
let updates = updateController?.availableUpdates()
|
||||
for update in updates! {
|
||||
if let installed = appLibrary.installedApp(forBundleId: update.bundleID) {
|
||||
// Display version of installed app compared to available update.
|
||||
print("\(update.itemIdentifier) \(update.title) (\(installed.bundleVersion) -> \(update.bundleVersion))")
|
||||
print("""
|
||||
\(update.itemIdentifier) \(update.title) (\(installed.bundleVersion) -> \(update.bundleVersion))
|
||||
""")
|
||||
} else {
|
||||
print("\(update.itemIdentifier) \(update.title) (unknown -> \(update.bundleVersion))")
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import Commandant
|
|||
import Result
|
||||
import CommerceKit
|
||||
|
||||
/// Kills several macOS processes as a means to reset the app store.
|
||||
public struct ResetCommand: CommandProtocol {
|
||||
public typealias Options = ResetOptions
|
||||
public let verb = "reset"
|
||||
|
@ -17,6 +18,7 @@ public struct ResetCommand: CommandProtocol {
|
|||
|
||||
public init() {}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
/*
|
||||
The "Reset Application" command in the Mac App Store debug menu performs
|
||||
|
@ -43,7 +45,7 @@ public struct ResetCommand: CommandProtocol {
|
|||
"storeassetd",
|
||||
"storedownloadd",
|
||||
"storeinstalld",
|
||||
"storelegacy",
|
||||
"storelegacy"
|
||||
]
|
||||
|
||||
let kill = Process()
|
||||
|
@ -85,8 +87,8 @@ public struct ResetOptions: OptionsProtocol {
|
|||
return ResetOptions(debug: debug)
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<ResetOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<ResetOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Switch(flag: nil, key: "debug", usage: "Enable debug mode")
|
||||
<*> mode <| Switch(flag: nil, key: "debug", usage: "Enable debug mode")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,15 +9,6 @@
|
|||
import Commandant
|
||||
import Result
|
||||
|
||||
struct ResultKeys {
|
||||
static let ResultCount = "resultCount"
|
||||
static let Results = "results"
|
||||
static let TrackName = "trackName"
|
||||
static let TrackId = "trackId"
|
||||
static let Version = "version"
|
||||
static let Price = "price"
|
||||
}
|
||||
|
||||
/// Search the Mac App Store using the iTunes Search API:
|
||||
/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/
|
||||
public struct SearchCommand: CommandProtocol {
|
||||
|
@ -25,66 +16,34 @@ public struct SearchCommand: CommandProtocol {
|
|||
public let verb = "search"
|
||||
public let function = "Search for apps from the Mac App Store"
|
||||
|
||||
private let urlSession: URLSession
|
||||
private let storeSearch: StoreSearch
|
||||
|
||||
public init(urlSession: URLSession = URLSession.shared) {
|
||||
self.urlSession = urlSession
|
||||
/// Designated initializer.
|
||||
///
|
||||
/// - Parameter storeSearch: Search manager.
|
||||
public init(storeSearch: StoreSearch = MasStoreSearch()) {
|
||||
self.storeSearch = storeSearch
|
||||
}
|
||||
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
guard let searchURLString = searchURLString(options.appName),
|
||||
let searchJson = urlSession.requestSynchronousJSONWithURLString(searchURLString) as? [String: Any] else {
|
||||
do {
|
||||
let resultList = try storeSearch.search(for: options.appName)
|
||||
if resultList.resultCount <= 0 || resultList.results.isEmpty {
|
||||
print("No results found")
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
let output = SearchResultFormatter.format(results: resultList.results, includePrice: options.price)
|
||||
print(output)
|
||||
|
||||
return .success(())
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
}
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
guard let resultCount = searchJson[ResultKeys.ResultCount] as? Int, resultCount > 0,
|
||||
let results = searchJson[ResultKeys.Results] as? [[String: Any]] else {
|
||||
print("No results found")
|
||||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
// find out longest appName for formatting
|
||||
var appNameMaxLength = 0
|
||||
for result in results {
|
||||
if let appName = result[ResultKeys.TrackName] as? String {
|
||||
if appName.count > appNameMaxLength {
|
||||
appNameMaxLength = appName.count
|
||||
}
|
||||
}
|
||||
}
|
||||
if appNameMaxLength > 50 {
|
||||
appNameMaxLength = 50
|
||||
}
|
||||
|
||||
for result in results {
|
||||
if let appName = result[ResultKeys.TrackName] as? String,
|
||||
let appVersion = result[ResultKeys.Version] as? String,
|
||||
let appId = result[ResultKeys.TrackId] as? Int,
|
||||
let appPrice = result[ResultKeys.Price] as? Double {
|
||||
|
||||
// add empty spaces to app name that every app name has the same length
|
||||
let countedAppName = String((appName + String(repeating: " ", count: appNameMaxLength)).prefix(appNameMaxLength))
|
||||
|
||||
if options.price {
|
||||
print(String(format:"%12d %@ $%5.2f (%@)", appId, countedAppName, appPrice, appVersion))
|
||||
} else {
|
||||
print(String(format:"%12d %@ (%@)", appId, countedAppName, appVersion))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return .success(())
|
||||
}
|
||||
|
||||
|
||||
/// Builds a URL to search the MAS for an app
|
||||
///
|
||||
/// - Parameter appName: Name of the app to find.
|
||||
/// - Returns: String URL for app search or nil if the app name could not be encoded.
|
||||
func searchURLString(_ appName: String) -> String? {
|
||||
guard let urlEncodedAppName = appName.URLEncodedString else { return nil }
|
||||
|
||||
return "https://itunes.apple.com/search?entity=macSoftware&term=\(urlEncodedAppName)&attribute=allTrackTerm"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,9 +57,9 @@ public struct SearchOptions: OptionsProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<SearchOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<SearchOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(usage: "the app name to search")
|
||||
<*> m <| Option(key: "price", defaultValue: false, usage: "Show price of found apps")
|
||||
<*> mode <| Argument(usage: "the app name to search")
|
||||
<*> mode <| Option(key: "price", defaultValue: false, usage: "Show price of found apps")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ public struct SignInCommand: CommandProtocol {
|
|||
|
||||
public init() {}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
|
||||
if #available(macOS 10.13, *) {
|
||||
return .failure(.signInDisabled)
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public struct SignInCommand: CommandProtocol {
|
|||
|
||||
do {
|
||||
printInfo("Signing in to Apple ID: \(options.username)")
|
||||
|
||||
|
||||
let password: String = {
|
||||
if options.password == "" && !options.dialog {
|
||||
return String(validatingUTF8: getpass("Password: "))!
|
||||
|
@ -37,11 +37,11 @@ public struct SignInCommand: CommandProtocol {
|
|||
return options.password
|
||||
}()
|
||||
|
||||
let _ = try ISStoreAccount.signIn(username: options.username, password: password, systemDialog: options.dialog)
|
||||
_ = try ISStoreAccount.signIn(username: options.username, password: password, systemDialog: options.dialog)
|
||||
} catch let error as NSError {
|
||||
return .failure(.signInFailed(error: error))
|
||||
}
|
||||
|
||||
|
||||
return .success(())
|
||||
}
|
||||
}
|
||||
|
@ -60,10 +60,10 @@ public struct SignInOptions: OptionsProtocol {
|
|||
}}
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<SignInOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<SignInOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(usage: "Apple ID")
|
||||
<*> m <| Argument(defaultValue: "", usage: "Password")
|
||||
<*> m <| Option(key: "dialog", defaultValue: false, usage: "Complete login with graphical dialog")
|
||||
<*> mode <| Argument(usage: "Apple ID")
|
||||
<*> mode <| Argument(defaultValue: "", usage: "Password")
|
||||
<*> mode <| Option(key: "dialog", defaultValue: false, usage: "Complete login with graphical dialog")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,13 +16,13 @@ public struct SignOutCommand: CommandProtocol {
|
|||
public let function = "Sign out of the Mac App Store"
|
||||
|
||||
public init() {}
|
||||
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
if #available(macOS 10.13, *) {
|
||||
let accountService: ISAccountService = ISServiceProxy.genericShared().accountService
|
||||
accountService.signOut()
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Using CKAccountStore to sign out does nothing on High Sierra
|
||||
// https://github.com/mas-cli/mas/issues/129
|
||||
CKAccountStore.shared().signOut()
|
||||
|
|
|
@ -46,8 +46,7 @@ public struct UninstallCommand: CommandProtocol {
|
|||
|
||||
do {
|
||||
try appLibrary.uninstallApp(app: product)
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
return .failure(.uninstallFailed)
|
||||
}
|
||||
|
||||
|
@ -69,9 +68,9 @@ public struct UninstallOptions: OptionsProtocol {
|
|||
}
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<UninstallOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<UninstallOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(usage: "ID of app to uninstall")
|
||||
<*> m <| Switch(flag: nil, key: "dry-run", usage: "dry run")
|
||||
<*> mode <| Argument(usage: "ID of app to uninstall")
|
||||
<*> mode <| Switch(flag: nil, key: "dry-run", usage: "dry run")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,11 +20,12 @@ public struct UpgradeCommand: CommandProtocol {
|
|||
|
||||
/// Designated initializer.
|
||||
///
|
||||
/// - Parameter appLibrary: <#appLibrary description#>
|
||||
/// - Parameter appLibrary: Instance of the app library.
|
||||
public init(appLibrary: AppLibrary = MasAppLibrary()) {
|
||||
self.appLibrary = appLibrary
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
let updateController = CKUpdateController.shared()
|
||||
let updates: [CKUpdate]
|
||||
|
@ -47,7 +48,7 @@ public struct UpgradeCommand: CommandProtocol {
|
|||
updates = appIds.compactMap {
|
||||
updateController?.availableUpdate(withItemIdentifier: $0)
|
||||
}
|
||||
|
||||
|
||||
guard updates.count > 0 else {
|
||||
printWarning("Nothing found to upgrade")
|
||||
return .success(())
|
||||
|
@ -61,10 +62,10 @@ public struct UpgradeCommand: CommandProtocol {
|
|||
return .success(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
print("Upgrading \(updates.count) outdated application\(updates.count > 1 ? "s" : ""):")
|
||||
print(updates.map({ "\($0.title) (\($0.bundleVersion))" }).joined(separator: ", "))
|
||||
|
||||
|
||||
let updateResults = updates.compactMap {
|
||||
download($0.itemIdentifier.uint64Value)
|
||||
}
|
||||
|
@ -82,13 +83,13 @@ public struct UpgradeCommand: CommandProtocol {
|
|||
|
||||
public struct UpgradeOptions: OptionsProtocol {
|
||||
let apps: [String]
|
||||
|
||||
|
||||
static func create(_ apps: [String]) -> UpgradeOptions {
|
||||
return UpgradeOptions(apps: apps)
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<UpgradeOptions, CommandantError<MASError>> {
|
||||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<UpgradeOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(defaultValue: [], usage: "app(s) to upgrade")
|
||||
<*> mode <| Argument(defaultValue: [], usage: "app(s) to upgrade")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,8 +36,11 @@ public struct VendorCommand: CommandProtocol {
|
|||
return .failure(.noSearchResultsFound)
|
||||
}
|
||||
|
||||
guard let vendorWebsite = result.sellerUrl
|
||||
else { throw MASError.noVendorWebsite }
|
||||
|
||||
do {
|
||||
try openCommand.run(arguments: result.sellerUrl)
|
||||
try openCommand.run(arguments: vendorWebsite)
|
||||
} catch {
|
||||
printError("Unable to launch open command")
|
||||
return .failure(.searchFailed)
|
||||
|
@ -47,8 +50,7 @@ public struct VendorCommand: CommandProtocol {
|
|||
printError("Open failed: (\(reason)) \(openCommand.stderr)")
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
} catch {
|
||||
// Bubble up MASErrors
|
||||
if let error = error as? MASError {
|
||||
return .failure(error)
|
||||
|
@ -67,8 +69,8 @@ public struct VendorOptions: OptionsProtocol {
|
|||
return VendorOptions(appId: appId)
|
||||
}
|
||||
|
||||
public static func evaluate(_ m: CommandMode) -> Result<VendorOptions, CommandantError<MASError>> {
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<VendorOptions, CommandantError<MASError>> {
|
||||
return create
|
||||
<*> m <| Argument(usage: "the app id to show the vendor's website")
|
||||
<*> mode <| Argument(usage: "the app id to show the vendor's website")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ public struct VersionCommand: CommandProtocol {
|
|||
|
||||
public init() {}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
let plist = Bundle.main.infoDictionary
|
||||
if let versionString = plist?["CFBundleShortVersionString"] {
|
||||
|
|
|
@ -43,21 +43,19 @@ public protocol AppLibrary {
|
|||
extension AppLibrary {
|
||||
/// Map of app name to ID.
|
||||
public var appIdsByName: [String: UInt64] {
|
||||
get {
|
||||
var destMap = [String: UInt64]()
|
||||
for product in installedApps {
|
||||
destMap[product.appName] = product.itemIdentifier.uint64Value
|
||||
}
|
||||
return destMap
|
||||
var destMap = [String: UInt64]()
|
||||
for product in installedApps {
|
||||
destMap[product.appName] = product.itemIdentifier.uint64Value
|
||||
}
|
||||
return destMap
|
||||
}
|
||||
|
||||
/// Finds an app by name.
|
||||
///
|
||||
/// - Parameter id: MAS ID for app.
|
||||
/// - Returns: Software Product of app if found; nil otherwise.
|
||||
public func installedApp(forId id: UInt64) -> SoftwareProduct? {
|
||||
let appId = NSNumber(value: id)
|
||||
public func installedApp(forId identifier: UInt64) -> SoftwareProduct? {
|
||||
let appId = NSNumber(value: identifier)
|
||||
return installedApps.first { $0.itemIdentifier == appId }
|
||||
}
|
||||
|
77
MasKit/Controllers/MasStoreSearch.swift
Normal file
77
MasKit/Controllers/MasStoreSearch.swift
Normal file
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// MasStoreSearch.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
/// Manages searching the MAS catalog through the iTunes Search and Lookup APIs.
|
||||
public class MasStoreSearch: StoreSearch {
|
||||
private let networkManager: NetworkManager
|
||||
|
||||
/// Designated initializer.
|
||||
public init(networkManager: NetworkManager = NetworkManager()) {
|
||||
self.networkManager = networkManager
|
||||
}
|
||||
|
||||
/// Searches for an app.
|
||||
///
|
||||
/// - Parameter appName: MAS ID of app
|
||||
/// - Returns: Search results list of app. List will have no records if there were no matches. Never nil.
|
||||
/// - Throws: Error if there is a problem with the network request.
|
||||
public func search(for appName: String) throws -> SearchResultList {
|
||||
guard let url = searchURL(for: appName)
|
||||
else { throw MASError.urlEncoding }
|
||||
|
||||
let result = networkManager.loadDataSync(from: url)
|
||||
|
||||
// Unwrap network result
|
||||
guard case let .success(data) = result
|
||||
else {
|
||||
if case let .failure(error) = result {
|
||||
throw error
|
||||
}
|
||||
throw MASError.noData
|
||||
}
|
||||
|
||||
do {
|
||||
let results = try JSONDecoder().decode(SearchResultList.self, from: data)
|
||||
return results
|
||||
} catch {
|
||||
throw MASError.jsonParsing(error: error as NSError)
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up app details.
|
||||
///
|
||||
/// - Parameter appId: MAS ID of app
|
||||
/// - Returns: Search result record of app or nil if no apps match the ID.
|
||||
/// - Throws: Error if there is a problem with the network request.
|
||||
public func lookup(app appId: String) throws -> SearchResult? {
|
||||
guard let url = lookupURL(forApp: appId)
|
||||
else { throw MASError.urlEncoding }
|
||||
|
||||
let result = networkManager.loadDataSync(from: url)
|
||||
|
||||
// Unwrap network result
|
||||
guard case let .success(data) = result
|
||||
else {
|
||||
if case let .failure(error) = result {
|
||||
throw error
|
||||
}
|
||||
throw MASError.noData
|
||||
}
|
||||
|
||||
do {
|
||||
let results = try JSONDecoder().decode(SearchResultList.self, from: data)
|
||||
|
||||
guard let searchResult = results.results.first
|
||||
else { return nil }
|
||||
|
||||
return searchResult
|
||||
} catch {
|
||||
throw MASError.jsonParsing(error: error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
54
MasKit/Controllers/StoreSearch.swift
Normal file
54
MasKit/Controllers/StoreSearch.swift
Normal file
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// StoreSearch.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
/// Protocol for searching the MAS catalog.
|
||||
public protocol StoreSearch {
|
||||
func lookup(app appId: String) throws -> SearchResult?
|
||||
func search(for appName: String) throws -> SearchResultList
|
||||
}
|
||||
|
||||
// MARK: - Common methods
|
||||
extension StoreSearch {
|
||||
/// Builds the search URL for an app.
|
||||
///
|
||||
/// - Parameter appName: MAS app identifier.
|
||||
/// - Returns: URL for the search service or nil if appName can't be encoded.
|
||||
public func searchURL(for appName: String) -> URL? {
|
||||
guard let urlString = searchURLString(forApp: appName) else { return nil }
|
||||
return URL(string: urlString)
|
||||
}
|
||||
|
||||
/// Builds the search URL for an app.
|
||||
///
|
||||
/// - Parameter appName: Name of app to find.
|
||||
/// - Returns: String URL for the search service or nil if appName can't be encoded.
|
||||
func searchURLString(forApp appName: String) -> String? {
|
||||
if let urlEncodedAppName = appName.URLEncodedString {
|
||||
return "https://itunes.apple.com/search?entity=macSoftware&term=\(urlEncodedAppName)&attribute=allTrackTerm"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Builds the lookup URL for an app.
|
||||
///
|
||||
/// - Parameter appId: MAS app identifier.
|
||||
/// - Returns: URL for the lookup service or nil if appId can't be encoded.
|
||||
public func lookupURL(forApp appId: String) -> URL? {
|
||||
guard let urlString = lookupURLString(forApp: appId) else { return nil }
|
||||
return URL(string: urlString)
|
||||
}
|
||||
|
||||
/// Builds the lookup URL for an app.
|
||||
///
|
||||
/// - Parameter appId: MAS app identifier.
|
||||
/// - Returns: String URL for the lookup service or nil if appId can't be encoded.
|
||||
func lookupURLString(forApp appId: String) -> String? {
|
||||
guard let urlEncodedAppId = appId.URLEncodedString else { return nil }
|
||||
return "https://itunes.apple.com/lookup?id=\(urlEncodedAppId)"
|
||||
}
|
||||
}
|
|
@ -21,10 +21,15 @@ public enum MASError: Error, CustomStringConvertible, Equatable {
|
|||
|
||||
case searchFailed
|
||||
case noSearchResultsFound
|
||||
case noVendorWebsite
|
||||
|
||||
case notInstalled
|
||||
case uninstallFailed
|
||||
|
||||
case urlEncoding
|
||||
case noData
|
||||
case jsonParsing(error: NSError?)
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .notSignedIn:
|
||||
|
@ -72,11 +77,23 @@ public enum MASError: Error, CustomStringConvertible, Equatable {
|
|||
case .noSearchResultsFound:
|
||||
return "No results found"
|
||||
|
||||
case .noVendorWebsite:
|
||||
return "App does not have a vendor website"
|
||||
|
||||
case .notInstalled:
|
||||
return "Not installed"
|
||||
|
||||
case .uninstallFailed:
|
||||
return "Uninstall failed"
|
||||
|
||||
case .urlEncoding:
|
||||
return "Unable to encode service URL"
|
||||
|
||||
case .noData:
|
||||
return "Service did not return data"
|
||||
|
||||
case .jsonParsing:
|
||||
return "Unable to parse response JSON"
|
||||
}
|
||||
}
|
||||
}
|
15
MasKit/Extensions/Dictionary+StringOrEmpty.swift
Normal file
15
MasKit/Extensions/Dictionary+StringOrEmpty.swift
Normal file
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// Dictionary+StringOrEmpty.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 1/7/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Dictionary {
|
||||
func stringOrEmpty(key: Key) -> String {
|
||||
return self[key] as? String ?? ""
|
||||
}
|
||||
}
|
16
MasKit/Extensions/String+PercentEncoding.swift
Normal file
16
MasKit/Extensions/String+PercentEncoding.swift
Normal file
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// String+PercentEncoding.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
/// Return an URL encoded string
|
||||
var URLEncodedString: String? {
|
||||
return addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||
}
|
||||
}
|
|
@ -27,27 +27,27 @@ public protocol ExternalCommand {
|
|||
|
||||
/// Common implementation
|
||||
extension ExternalCommand {
|
||||
public var stdout: String { get {
|
||||
public var stdout: String {
|
||||
let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
return String(data: data, encoding: .utf8) ?? ""
|
||||
}}
|
||||
}
|
||||
|
||||
public var stderr: String { get {
|
||||
public var stderr: String {
|
||||
let data = stderrPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
return String(data: data, encoding: .utf8) ?? ""
|
||||
}}
|
||||
}
|
||||
|
||||
public var exitCode: Int? { get {
|
||||
public var exitCode: Int? {
|
||||
return Int(process.terminationStatus)
|
||||
}}
|
||||
}
|
||||
|
||||
public var succeeded: Bool { get {
|
||||
public var succeeded: Bool {
|
||||
return exitCode == 0
|
||||
}}
|
||||
}
|
||||
|
||||
public var failed: Bool { get {
|
||||
public var failed: Bool {
|
||||
return !succeeded
|
||||
}}
|
||||
}
|
||||
|
||||
/// Runs the command.
|
||||
public func run(arguments: String...) throws {
|
||||
|
|
57
MasKit/Formatters/AppInfoFormatter.swift
Normal file
57
MasKit/Formatters/AppInfoFormatter.swift
Normal file
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// AppInfoFormatter.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 1/7/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Formats text output for the info command.
|
||||
struct AppInfoFormatter {
|
||||
/// Formats text output with app info.
|
||||
///
|
||||
/// - Parameter app: Search result with app data.
|
||||
/// - Returns: Multiline text output.
|
||||
static func format(app: SearchResult) -> String {
|
||||
let headline = [
|
||||
"\(app.trackName)",
|
||||
"\(app.version)",
|
||||
"[\(app.price)]"
|
||||
].joined(separator: " ")
|
||||
|
||||
return [
|
||||
headline,
|
||||
"By: \(app.sellerName)",
|
||||
"Released: \(humanReadableDate(app.currentVersionReleaseDate))",
|
||||
"Minimum OS: \(app.minimumOsVersion)",
|
||||
"Size: \(humanReadableSize(app.fileSizeBytes))",
|
||||
"From: \(app.trackViewUrl)"
|
||||
].joined(separator: "\n")
|
||||
}
|
||||
|
||||
/// Formats a file size.
|
||||
///
|
||||
/// - Parameter size: Numeric string.
|
||||
/// - Returns: Formatted file size description.
|
||||
private static func humanReadableSize(_ size: String) -> String {
|
||||
let bytesSize = Int64(size) ?? 0
|
||||
return ByteCountFormatter.string(fromByteCount: bytesSize, countStyle: .file)
|
||||
}
|
||||
|
||||
/// Formats a date in format.
|
||||
///
|
||||
/// - Parameter serverDate: String containing a date in ISO-8601 format.
|
||||
/// - Returns: Simple date format.
|
||||
private static func humanReadableDate(_ serverDate: String) -> String {
|
||||
let serverDateFormatter = DateFormatter()
|
||||
serverDateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
serverDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
|
||||
|
||||
let humanDateFormatter = DateFormatter()
|
||||
humanDateFormatter.timeStyle = .none
|
||||
humanDateFormatter.dateStyle = .medium
|
||||
return serverDateFormatter.date(from: serverDate).flatMap(humanDateFormatter.string(from:)) ?? ""
|
||||
}
|
||||
}
|
39
MasKit/Formatters/SearchResultFormatter.swift
Normal file
39
MasKit/Formatters/SearchResultFormatter.swift
Normal file
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// SearchResultFormatter.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 1/11/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Formats text output for the search command.
|
||||
struct SearchResultFormatter {
|
||||
/// Formats text output with search results.
|
||||
///
|
||||
/// - Parameter results: Search results with app data
|
||||
/// - Returns: Multiliune text outoutp.
|
||||
static func format(results: [SearchResult], includePrice: Bool = false) -> String {
|
||||
// find longest appName for formatting, default 50
|
||||
let maxLength = results.map { $0.trackName }.max(by: { $1.count > $0.count })?.count
|
||||
?? 50
|
||||
|
||||
var output: String = ""
|
||||
|
||||
for result in results {
|
||||
let appId = result.trackId
|
||||
let appName = result.trackName.padding(toLength: maxLength, withPad: " ", startingAt: 0)
|
||||
let version = result.version
|
||||
let price = result.price
|
||||
|
||||
if includePrice {
|
||||
output += String(format: "%12d %@ $%5.2f (%@)", appId, appName, price, version)
|
||||
} else {
|
||||
output += String(format: "%12d %@ (%@)", appId, appName, version)
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@
|
|||
|
||||
/// A collection of output formatting helpers
|
||||
|
||||
|
||||
/// Terminal Control Sequence Indicator
|
||||
let csi = "\u{001B}["
|
||||
|
||||
|
@ -17,7 +16,7 @@ func printInfo(_ message: String) {
|
|||
print("==> \(message)")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Blue bold arrow, Bold text
|
||||
print("\(csi)1;34m==>\(csi)0m \(csi)1m\(message)\(csi)0m")
|
||||
}
|
||||
|
@ -27,7 +26,7 @@ func printWarning(_ message: String) {
|
|||
print("Warning: \(message)")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Yellow, underlined "Warning:" prefix
|
||||
print("\(csi)4;33mWarning:\(csi)0m \(message)")
|
||||
}
|
||||
|
@ -37,7 +36,7 @@ public func printError(_ message: String) {
|
|||
print("Warning: \(message)")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Red, underlined "Error:" prefix
|
||||
print("\(csi)4;31mError:\(csi)0m \(message)")
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
//
|
||||
// MasStoreSearch.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
/// Manages searching the MAS catalog through the iTunes Search and Lookup APIs.
|
||||
public class MasStoreSearch: StoreSearch {
|
||||
private let urlSession: URLSession
|
||||
|
||||
/// Designated initializer.
|
||||
public init(urlSession: URLSession = URLSession.shared) {
|
||||
self.urlSession = urlSession
|
||||
}
|
||||
|
||||
/// Looks up app details.
|
||||
///
|
||||
/// - Parameter appId: MAS ID of app
|
||||
/// - Returns: Search result record of app or nil if no apps match the ID.
|
||||
/// - Throws: Error if there is a problem with the network request.
|
||||
public func lookup(app appId: String) throws -> SearchResult? {
|
||||
guard let lookupURLString = lookupURLString(forApp: appId),
|
||||
let jsonData = urlSession.requestSynchronousDataWithURLString(lookupURLString)
|
||||
else {
|
||||
// network error
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
|
||||
guard let results = try? JSONDecoder().decode(SearchResultList.self, from: jsonData)
|
||||
else {
|
||||
// parse error
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
|
||||
guard let result = results.results.first
|
||||
else { return nil }
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
52
MasKit/Models/SearchResult.swift
Normal file
52
MasKit/Models/SearchResult.swift
Normal file
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// SearchResult.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
public struct SearchResult: Decodable {
|
||||
public var bundleId: String
|
||||
public var currentVersionReleaseDate: String
|
||||
public var fileSizeBytes: String
|
||||
public var formattedPrice: String
|
||||
public var minimumOsVersion: String
|
||||
public var price: Double
|
||||
public var sellerName: String
|
||||
public var sellerUrl: String?
|
||||
public var trackId: Int
|
||||
public var trackCensoredName: String
|
||||
public var trackName: String
|
||||
public var trackViewUrl: String
|
||||
public var version: String
|
||||
|
||||
init(bundleId: String = "",
|
||||
currentVersionReleaseDate: String = "",
|
||||
fileSizeBytes: String = "",
|
||||
formattedPrice: String = "",
|
||||
minimumOsVersion: String = "",
|
||||
price: Double = 0.0,
|
||||
sellerName: String = "",
|
||||
sellerUrl: String = "",
|
||||
trackId: Int = 0,
|
||||
trackCensoredName: String = "",
|
||||
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.trackCensoredName = trackCensoredName
|
||||
self.trackName = trackName
|
||||
self.trackViewUrl = trackViewUrl
|
||||
self.version = version
|
||||
}
|
||||
}
|
|
@ -6,7 +6,7 @@
|
|||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
struct SearchResultList: Decodable {
|
||||
public struct SearchResultList: Decodable {
|
||||
var resultCount: Int
|
||||
var results: [SearchResult]
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
//
|
||||
// NSURLSession+Synchronous.swift
|
||||
// mas-cli
|
||||
//
|
||||
// Created by Michael Schneider on 4/14/16.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
//
|
||||
|
||||
// Synchronous NSURLSession code found at: http://ericasadun.com/2015/11/12/more-bad-things-synchronous-nsurlsessions/
|
||||
|
||||
import Foundation
|
||||
|
||||
/// NSURLSession synchronous behavior
|
||||
/// Particularly for playground sessions that need to run sequentially
|
||||
public extension URLSession {
|
||||
/// Return data from synchronous URL request
|
||||
public func requestSynchronousData(_ request: URLRequest) -> Data? {
|
||||
var data: Data? = nil
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
let task = URLSession.shared.dataTask(with: request) {
|
||||
taskData, _, error -> () in
|
||||
data = taskData
|
||||
if data == nil, let error = error {print(error)}
|
||||
semaphore.signal()
|
||||
}
|
||||
task.resume()
|
||||
let _ = semaphore.wait(timeout: .distantFuture)
|
||||
return data
|
||||
}
|
||||
|
||||
/// Return data synchronous from specified endpoint
|
||||
public func requestSynchronousDataWithURLString(_ requestString: String) -> Data? {
|
||||
guard let url = URL(string:requestString) else {return nil}
|
||||
let request = URLRequest(url: url)
|
||||
return requestSynchronousData(request)
|
||||
}
|
||||
|
||||
/// Return JSON synchronous from URL request
|
||||
public func requestSynchronousJSON(_ request: URLRequest) -> Any? {
|
||||
guard let data = requestSynchronousData(request) else {return nil}
|
||||
return try! JSONSerialization.jsonObject(with: data, options: [])
|
||||
}
|
||||
|
||||
/// Return JSON synchronous from specified endpoint
|
||||
@objc public func requestSynchronousJSONWithURLString(_ requestString: String) -> Any? {
|
||||
guard let url = URL(string: requestString) else {return nil}
|
||||
var request = URLRequest(url:url)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
return requestSynchronousJSON(request)
|
||||
}
|
||||
}
|
||||
|
||||
public extension String {
|
||||
/// Return an URL encoded string
|
||||
var URLEncodedString: String? {
|
||||
return addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||
}
|
||||
}
|
60
MasKit/Network/NetworkManager.swift
Normal file
60
MasKit/Network/NetworkManager.swift
Normal file
|
@ -0,0 +1,60 @@
|
|||
//
|
||||
// NetworkManager.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Network abstraction
|
||||
public class NetworkManager {
|
||||
enum NetworkError: Error {
|
||||
case timeout
|
||||
}
|
||||
|
||||
private let session: NetworkSession
|
||||
|
||||
/// Designated initializer
|
||||
///
|
||||
/// - Parameter session: A networking session.
|
||||
public init(session: NetworkSession = URLSession.shared) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
/// Loads data asynchronously.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: URL to load data from.
|
||||
/// - completionHandler: Closure where result is delivered.
|
||||
func loadData(from url: URL, completionHandler: @escaping (NetworkResult) -> Void) {
|
||||
session.loadData(from: url) { (data: Data?, error: Error?) in
|
||||
let result: NetworkResult = data != nil
|
||||
? .success(data!)
|
||||
: .failure(error!)
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads data synchronously.
|
||||
///
|
||||
/// - Parameter url: URL to load data from.
|
||||
/// - Returns: Network result containing either Data or an Error.
|
||||
func loadDataSync(from url: URL) -> NetworkResult {
|
||||
var syncResult: NetworkResult?
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
loadData(from: url) { asyncResult in
|
||||
syncResult = asyncResult
|
||||
semaphore.signal()
|
||||
}
|
||||
_ = semaphore.wait(timeout: .distantFuture)
|
||||
|
||||
guard let result = syncResult else {
|
||||
return .failure(NetworkError.timeout)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
30
MasKit/Network/NetworkResult.swift
Normal file
30
MasKit/Network/NetworkResult.swift
Normal file
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// NetworkResult.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
enum NetworkResult {
|
||||
case success(Data)
|
||||
case failure(Error)
|
||||
}
|
||||
|
||||
extension NetworkResult: Equatable {
|
||||
static func == (lhs: NetworkResult, rhs: NetworkResult) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (let .success(data1), let .success(data2)):
|
||||
return data1 == data2
|
||||
|
||||
case (let .failure(error1), let .failure(error2)):
|
||||
return error1.localizedDescription == error2.localizedDescription
|
||||
|
||||
// case (.none, .none):
|
||||
// return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
11
MasKit/Network/NetworkSession.swift
Normal file
11
MasKit/Network/NetworkSession.swift
Normal file
|
@ -0,0 +1,11 @@
|
|||
//
|
||||
// NetworkSession.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
@objc public protocol NetworkSession {
|
||||
@objc func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void)
|
||||
}
|
18
MasKit/Network/URLSession+NetworkSession.swift
Normal file
18
MasKit/Network/URLSession+NetworkSession.swift
Normal file
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// URLSession+NetworkSession.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URLSession: NetworkSession {
|
||||
@objc open func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
let task = dataTask(with: url) { (data, _, error) in
|
||||
completionHandler(data, error)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
//
|
||||
// SearchResult.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
public struct SearchResult: Decodable {
|
||||
public var bundleId: String
|
||||
public var price: Double
|
||||
public var sellerName: String
|
||||
public var sellerUrl: String
|
||||
public var trackId: Int
|
||||
public var trackName: String
|
||||
public var trackViewUrl: String
|
||||
public var version: String
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
//
|
||||
// StoreSearch.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
/// Protocol for searching the MAS catalog.
|
||||
public protocol StoreSearch {
|
||||
func lookupURLString(forApp: String) -> String?
|
||||
func lookup(app appId: String) throws -> SearchResult?
|
||||
}
|
||||
|
||||
extension StoreSearch {
|
||||
/// Builds the lookup URL for an app.
|
||||
///
|
||||
/// - Parameter appId: MAS app identifier.
|
||||
/// - Returns: A string URL for the lookup service or nil if the appId can't be encoded.
|
||||
public func lookupURLString(forApp appId: String) -> String? {
|
||||
if let urlEncodedAppId = appId.URLEncodedString {
|
||||
return "https://itunes.apple.com/lookup?id=\(urlEncodedAppId)"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -14,17 +14,12 @@ import Nimble
|
|||
class HomeCommandSpec: QuickSpec {
|
||||
override func spec() {
|
||||
let result = SearchResult(
|
||||
bundleId: "",
|
||||
price: 0.0,
|
||||
sellerName: "",
|
||||
sellerUrl: "",
|
||||
trackId: 1111,
|
||||
trackName: "",
|
||||
trackViewUrl: "mas preview url",
|
||||
version: "0.0"
|
||||
)
|
||||
let storeSearch = MockStoreSearch()
|
||||
let openCommand = MockOpenSystemCommand()
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let cmd = HomeCommand(storeSearch: storeSearch, openCommand: openCommand)
|
||||
|
||||
describe("home command") {
|
||||
|
|
|
@ -13,12 +13,58 @@ import Nimble
|
|||
|
||||
class InfoCommandSpec: QuickSpec {
|
||||
override func spec() {
|
||||
let result = SearchResult(
|
||||
currentVersionReleaseDate: "2019-01-07T18:53:13Z",
|
||||
fileSizeBytes: "1024",
|
||||
minimumOsVersion: "10.14",
|
||||
price: 2.0,
|
||||
sellerName: "Awesome Dev",
|
||||
trackId: 1111,
|
||||
trackName: "Awesome App",
|
||||
trackViewUrl: "https://awesome.app",
|
||||
version: "1.0"
|
||||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
let cmd = InfoCommand(storeSearch: storeSearch)
|
||||
let expectedOutput = """
|
||||
Awesome App 1.0 [2.0]
|
||||
By: Awesome Dev
|
||||
Released: Jan 7, 2019
|
||||
Minimum OS: 10.14
|
||||
Size: 1 KB
|
||||
From: https://awesome.app
|
||||
|
||||
"""
|
||||
|
||||
describe("Info command") {
|
||||
beforeEach {
|
||||
storeSearch.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
let result = cmd.run(InfoCommand.Options(appId: "-999"))
|
||||
expect(result).to(beFailure { error in
|
||||
expect(error) == .searchFailed
|
||||
})
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
let result = cmd.run(InfoCommand.Options(appId: "999"))
|
||||
expect(result).to(beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
})
|
||||
}
|
||||
it("displays app details") {
|
||||
let cmd = InfoCommand()
|
||||
let result = cmd.run(InfoCommand.Options(appId: ""))
|
||||
print(result)
|
||||
// expect(result).to(beSuccess())
|
||||
storeSearch.apps[result.trackId] = result
|
||||
let output = OutputListener()
|
||||
output.openConsolePipe()
|
||||
|
||||
let result = cmd.run(InfoCommand.Options(appId: result.trackId.description))
|
||||
|
||||
expect(result).to(beSuccess())
|
||||
// output is async so need to wait for contents to be updated
|
||||
expect(output.contents).toEventuallyNot(beEmpty())
|
||||
expect(output.contents) == expectedOutput
|
||||
|
||||
output.closeConsolePipe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,17 +14,12 @@ import Nimble
|
|||
class OpenCommandSpec: QuickSpec {
|
||||
override func spec() {
|
||||
let result = SearchResult(
|
||||
bundleId: "",
|
||||
price: 0.0,
|
||||
sellerName: "",
|
||||
sellerUrl: "",
|
||||
trackId: 1111,
|
||||
trackName: "",
|
||||
trackViewUrl: "fakescheme://some/url",
|
||||
version: "0.0"
|
||||
)
|
||||
let storeSearch = MockStoreSearch()
|
||||
let openCommand = MockOpenSystemCommand()
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let cmd = OpenCommand(storeSearch: storeSearch, openCommand: openCommand)
|
||||
|
||||
describe("open command") {
|
||||
|
|
|
@ -13,12 +13,33 @@ import Nimble
|
|||
|
||||
class SearchCommandSpec: QuickSpec {
|
||||
override func spec() {
|
||||
let result = SearchResult(
|
||||
trackId: 1111,
|
||||
trackName: "slack",
|
||||
trackViewUrl: "mas preview url",
|
||||
version: "0.0"
|
||||
)
|
||||
let storeSearch = StoreSearchMock()
|
||||
|
||||
describe("search command") {
|
||||
it("updates stuff") {
|
||||
let cmd = SearchCommand()
|
||||
let result = cmd.run(SearchCommand.Options(appName: "", price: false))
|
||||
print(result)
|
||||
// expect(result).to(beSuccess())
|
||||
beforeEach {
|
||||
storeSearch.reset()
|
||||
}
|
||||
it("can find slack") {
|
||||
storeSearch.apps[result.trackId] = result
|
||||
|
||||
let search = SearchCommand(storeSearch: storeSearch)
|
||||
let searchOptions = SearchOptions(appName: "slack", price: false)
|
||||
let result = search.run(searchOptions)
|
||||
expect(result).to(beSuccess())
|
||||
}
|
||||
it("fails when searching for nonexistent app") {
|
||||
let search = SearchCommand(storeSearch: storeSearch)
|
||||
let searchOptions = SearchOptions(appName: "nonexistent", price: false)
|
||||
let result = search.run(searchOptions)
|
||||
expect(result).to(beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,62 +0,0 @@
|
|||
//
|
||||
// SearchSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 11/12/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import MasKit
|
||||
import Result
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
class SearchSpec: QuickSpec {
|
||||
override func spec() {
|
||||
describe("search") {
|
||||
context("for slack") {
|
||||
it("succeeds") {
|
||||
let search = SearchCommand(urlSession: MockURLSession(responseFile: "search/slack.json"))
|
||||
let searchOptions = SearchOptions(appName: "slack", price: false)
|
||||
let result = search.run(searchOptions)
|
||||
expect(result).to(beSuccess())
|
||||
}
|
||||
}
|
||||
context("for nonexistent") {
|
||||
it("fails") {
|
||||
let search = SearchCommand(urlSession: MockURLSession(responseFile: "search/nonexistent.json"))
|
||||
let searchOptions = SearchOptions(appName: "nonexistent", price: false)
|
||||
let result = search.run(searchOptions)
|
||||
expect(result).to(beFailure { error in
|
||||
expect(error) == .noSearchResultsFound
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
describe("url string") {
|
||||
it("contains the app name") {
|
||||
let appName = "myapp"
|
||||
let search = SearchCommand()
|
||||
let urlString = search.searchURLString(appName)
|
||||
expect(urlString) ==
|
||||
"https://itunes.apple.com/search?entity=macSoftware&term=\(appName)&attribute=allTrackTerm"
|
||||
}
|
||||
it("contains the encoded app name") {
|
||||
let appName = "My App"
|
||||
let appNameEncoded = "My%20App"
|
||||
let search = SearchCommand()
|
||||
let urlString = search.searchURLString(appName)
|
||||
expect(urlString) ==
|
||||
"https://itunes.apple.com/search?entity=macSoftware&term=\(appNameEncoded)&attribute=allTrackTerm"
|
||||
}
|
||||
// FIXME: Find a character that causes addingPercentEncoding(withAllowedCharacters to return nil
|
||||
xit("is nil when app name cannot be url encoded") {
|
||||
let appName = "`~!@#$%^&*()_+ 💩"
|
||||
let search = SearchCommand()
|
||||
let urlString = search.searchURLString(appName)
|
||||
expect(urlString).to(beNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,13 +15,13 @@ class UninstallCommandSpec: QuickSpec {
|
|||
override func spec() {
|
||||
describe("uninstall command") {
|
||||
let appId = 12345
|
||||
let app = MockSoftwareProduct(
|
||||
let app = SoftwareProductMock(
|
||||
appName: "Some App",
|
||||
bundlePath: "/tmp/Some.app",
|
||||
bundleVersion: "1.0",
|
||||
itemIdentifier: NSNumber(value: appId)
|
||||
)
|
||||
let mockLibrary = MockAppLibrary()
|
||||
let mockLibrary = AppLibraryMock()
|
||||
let uninstall = UninstallCommand(appLibrary: mockLibrary)
|
||||
|
||||
context("dry run") {
|
||||
|
|
|
@ -14,17 +14,12 @@ import Nimble
|
|||
class VendorCommandSpec: QuickSpec {
|
||||
override func spec() {
|
||||
let result = SearchResult(
|
||||
bundleId: "",
|
||||
price: 0.0,
|
||||
sellerName: "",
|
||||
sellerUrl: "",
|
||||
trackId: 1111,
|
||||
trackName: "",
|
||||
trackViewUrl: "https://awesome.app",
|
||||
version: "0.0"
|
||||
)
|
||||
let storeSearch = MockStoreSearch()
|
||||
let openCommand = MockOpenSystemCommand()
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let cmd = VendorCommand(storeSearch: storeSearch, openCommand: openCommand)
|
||||
|
||||
describe("vendor command") {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MockAppLibrary.swift
|
||||
// AppLibraryMock.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
|
@ -8,7 +8,7 @@
|
|||
|
||||
@testable import MasKit
|
||||
|
||||
class MockAppLibrary: AppLibrary {
|
||||
class AppLibraryMock: AppLibrary {
|
||||
var installedApps = [SoftwareProduct]()
|
||||
|
||||
/// Finds an app using a bundle identifier.
|
||||
|
@ -34,7 +34,7 @@ class MockAppLibrary: AppLibrary {
|
|||
}
|
||||
|
||||
/// Members not part of the AppLibrary protocol that are only for test state managment.
|
||||
extension MockAppLibrary {
|
||||
extension AppLibraryMock {
|
||||
/// Clears out the list of installed apps.
|
||||
func reset() {
|
||||
installedApps = []
|
63
MasKitTests/Controllers/MasStoreSearchSpec.swift
Normal file
63
MasKitTests/Controllers/MasStoreSearchSpec.swift
Normal file
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// MasStoreSearchSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/4/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import MasKit
|
||||
import Result
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
class MasStoreSearchSpec: QuickSpec {
|
||||
override func spec() {
|
||||
describe("store search") {
|
||||
it("can find slack") {
|
||||
let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json")
|
||||
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession))
|
||||
|
||||
var searchList: SearchResultList
|
||||
do {
|
||||
searchList = try storeSearch.search(for: "slack")
|
||||
expect(searchList.resultCount) == 6
|
||||
expect(searchList.results.count) == 6
|
||||
} catch {
|
||||
let maserror = error as! MASError
|
||||
if case .jsonParsing(let nserror) = maserror {
|
||||
fail("\(maserror) \(nserror!)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
describe("store lookup") {
|
||||
it("can find slack") {
|
||||
let appId = 803453959
|
||||
let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json")
|
||||
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession))
|
||||
|
||||
var lookup: SearchResult?
|
||||
do {
|
||||
lookup = try storeSearch.lookup(app: appId.description)
|
||||
} catch {
|
||||
let maserror = error as! MASError
|
||||
if case .jsonParsing(let nserror) = maserror {
|
||||
fail("\(maserror) \(nserror!)")
|
||||
}
|
||||
}
|
||||
|
||||
guard let result = lookup else { fatalError("lookup result was nil") }
|
||||
|
||||
expect(result.trackId) == appId
|
||||
expect(result.bundleId) == "com.tinyspeck.slackmacgap"
|
||||
expect(result.price) == 0
|
||||
expect(result.sellerName) == "Slack Technologies, Inc."
|
||||
expect(result.sellerUrl) == "https://slack.com"
|
||||
expect(result.trackName) == "Slack"
|
||||
expect(result.trackViewUrl) == "https://itunes.apple.com/us/app/slack/id803453959?mt=12&uo=4"
|
||||
expect(result.version) == "3.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MockStoreSearch.swift
|
||||
// StoreSearchMock.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/4/19.
|
||||
|
@ -8,9 +8,14 @@
|
|||
|
||||
@testable import MasKit
|
||||
|
||||
class MockStoreSearch: StoreSearch {
|
||||
class StoreSearchMock: StoreSearch {
|
||||
var apps: [Int: SearchResult] = [:]
|
||||
|
||||
func search(for appName: String) throws -> SearchResultList {
|
||||
let filtered = apps.filter { $1.trackName.contains(appName) }
|
||||
return SearchResultList(resultCount: filtered.count, results: filtered.map { $1 })
|
||||
}
|
||||
|
||||
func lookup(app appId: String) throws -> SearchResult? {
|
||||
guard let number = Int(appId)
|
||||
else { throw MASError.searchFailed }
|
45
MasKitTests/Controllers/StoreSearchSpec.swift
Normal file
45
MasKitTests/Controllers/StoreSearchSpec.swift
Normal file
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// StoreSearchSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/11/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
@testable import MasKit
|
||||
import Result
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
/// Protocol minimal implementation
|
||||
struct StoreSearchForTesting: StoreSearch {
|
||||
func lookup(app appId: String) throws -> SearchResult? { return nil }
|
||||
func search(for appName: String) throws -> SearchResultList { return SearchResultList(resultCount: 0, results: []) }
|
||||
}
|
||||
|
||||
class StoreSearchSpec: QuickSpec {
|
||||
override func spec() {
|
||||
let storeSearch = StoreSearchForTesting()
|
||||
|
||||
describe("url string") {
|
||||
it("contains the app name") {
|
||||
let appName = "myapp"
|
||||
let urlString = storeSearch.searchURLString(forApp: appName)
|
||||
expect(urlString) ==
|
||||
"https://itunes.apple.com/search?entity=macSoftware&term=\(appName)&attribute=allTrackTerm"
|
||||
}
|
||||
it("contains the encoded app name") {
|
||||
let appName = "My App"
|
||||
let appNameEncoded = "My%20App"
|
||||
let urlString = storeSearch.searchURLString(forApp: appName)
|
||||
expect(urlString) ==
|
||||
"https://itunes.apple.com/search?entity=macSoftware&term=\(appNameEncoded)&attribute=allTrackTerm"
|
||||
}
|
||||
// Find a character that causes addingPercentEncoding(withAllowedCharacters to return nil
|
||||
xit("is nil when app name cannot be url encoded") {
|
||||
let appName = "`~!@#$%^&*()_+ 💩"
|
||||
let urlString = storeSearch.searchURLString(forApp: appName)
|
||||
expect(urlString).to(beNil())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -99,6 +99,11 @@ class MASErrorTestCase: XCTestCase {
|
|||
XCTAssertEqual(error.description, "No results found")
|
||||
}
|
||||
|
||||
func testNoVendorWebsite() {
|
||||
error = .noVendorWebsite
|
||||
XCTAssertEqual(error.description, "App does not have a vendor website")
|
||||
}
|
||||
|
||||
func testNotInstalled() {
|
||||
error = .notInstalled
|
||||
XCTAssertEqual(error.description, "Not installed")
|
||||
|
@ -108,4 +113,19 @@ class MASErrorTestCase: XCTestCase {
|
|||
error = .uninstallFailed
|
||||
XCTAssertEqual(error.description, "Uninstall failed")
|
||||
}
|
||||
|
||||
func testUrlEncoding() {
|
||||
error = .urlEncoding
|
||||
XCTAssertEqual(error.description, "Unable to encode service URL")
|
||||
}
|
||||
|
||||
func testNoData() {
|
||||
error = .noData
|
||||
XCTAssertEqual(error.description, "Service did not return data")
|
||||
}
|
||||
|
||||
func testJsonParsing() {
|
||||
error = .jsonParsing(error: nil)
|
||||
XCTAssertEqual(error.description, "Unable to parse response JSON")
|
||||
}
|
||||
}
|
32
MasKitTests/Extensions/Bundle+JSON.swift
Normal file
32
MasKitTests/Extensions/Bundle+JSON.swift
Normal file
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// Bundle+JSON.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Bundle {
|
||||
/// Locates a JSON response file from the test bundle.
|
||||
///
|
||||
/// - Parameter fileName: Name of file to locate.
|
||||
/// - Returns: URL to file.
|
||||
static func jsonResponse(fileName: String) -> URL? {
|
||||
return Bundle(for: NetworkSessionMock.self).fileURL(fileName: fileName)
|
||||
}
|
||||
|
||||
/// Builds a URL for a file in the JSON directory of the current bundle.
|
||||
///
|
||||
/// - Parameter fileName: Name of file to locate.
|
||||
/// - Returns: URL to file.
|
||||
func fileURL(fileName: String) -> URL? {
|
||||
guard let path = self.path(forResource: fileName.fileNameWithoutExtension,
|
||||
ofType: fileName.fileExtension,
|
||||
inDirectory: "JSON")
|
||||
else { fatalError("Unable to load file \(fileName)") }
|
||||
|
||||
return URL(fileURLWithPath: path)
|
||||
}
|
||||
}
|
21
MasKitTests/Extensions/String+FileExtension.swift
Normal file
21
MasKitTests/Extensions/String+FileExtension.swift
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// String+FileExtension.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
/// Returns the file name before the extension.
|
||||
var fileNameWithoutExtension: String {
|
||||
return (self as NSString).deletingPathExtension
|
||||
}
|
||||
|
||||
/// Returns the file extension.
|
||||
var fileExtension: String {
|
||||
return (self as NSString).pathExtension
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MockOpenSystemCommand.swift
|
||||
// OpenSystemCommandMock.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/4/19.
|
||||
|
@ -8,7 +8,7 @@
|
|||
|
||||
@testable import MasKit
|
||||
|
||||
class MockOpenSystemCommand: ExternalCommand {
|
||||
class OpenSystemCommandMock: ExternalCommand {
|
||||
// Stub out protocol logic
|
||||
var succeeded = true
|
||||
var arguments: [String]?
|
|
@ -1,40 +0,0 @@
|
|||
//
|
||||
// MasStoreSearchSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/4/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import MasKit
|
||||
import Result
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
class MasStoreSearchSpec: QuickSpec {
|
||||
override func spec() {
|
||||
let appId = 803453959
|
||||
let urlSession = MockURLSession(responseFile: "lookup/slack.json")
|
||||
let storeSearch = MasStoreSearch(urlSession: urlSession)
|
||||
describe("store search") {
|
||||
it("can find slack") {
|
||||
// FIXME: Doesn't work offline
|
||||
// 2019-01-05 08:37:33.764724-0700 xctest[76864:1854467] TIC TCP Conn Failed [1:0x100c90f90]: 1:50 Err(50)
|
||||
// 2019-01-05 08:37:33.774861-0700 xctest[76864:1854467] Task <8D1421BF-F9A3-4716-BCB0-803438C7E3E8>.<1> HTTP load failed (error code: -1009 [1:50])
|
||||
// 2019-01-05 08:37:33.774983-0700 xctest[76864:1854467] Task <8D1421BF-F9A3-4716-BCB0-803438C7E3E8>.<1> finished with error - code: -1009
|
||||
// Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline."
|
||||
let result = try! storeSearch.lookup(app: appId.description)
|
||||
expect(result).toNot(beNil())
|
||||
expect(result!.trackId) == appId
|
||||
|
||||
expect(result!.bundleId) == "com.tinyspeck.slackmacgap"
|
||||
expect(result!.price) == 0
|
||||
expect(result!.sellerName) == "Slack Technologies, Inc."
|
||||
expect(result!.sellerUrl) == "https://slack.com"
|
||||
expect(result!.trackName) == "Slack"
|
||||
expect(result!.trackViewUrl) == "https://itunes.apple.com/us/app/slack/id803453959?mt=12&uo=4"
|
||||
expect(result!.version) == "3.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
//
|
||||
// MockURLSession.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 11/13/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
/// Mock URLSession for testing.
|
||||
// FIXME: allow mock url session to operate offline
|
||||
//2019-01-04 17:20:41.741632-0800 xctest[76410:1817605] TIC TCP Conn Failed [3:0x100a67420]: 1:50 Err(50)
|
||||
//2019-01-04 17:20:41.741849-0800 xctest[76410:1817605] Task <0C05E774-1CDE-48FB-9408-AFFCD12F3F60>.<3> HTTP load failed (error code: -1009 [1:50])
|
||||
//2019-01-04 17:20:41.741903-0800 xctest[76410:1817605] Task <0C05E774-1CDE-48FB-9408-AFFCD12F3F60>.<3> finished with error - code: -1009
|
||||
//Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline." UserInfo={NSUnderlyingError=0x100a692f0 {Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)" UserInfo={_kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}}, NSErrorFailingURLStringKey=https://itunes.apple.com/lookup?id=803453959, NSErrorFailingURLKey=https://itunes.apple.com/lookup?id=803453959, _kCFStreamErrorDomainKey=1, _kCFStreamErrorCodeKey=50, NSLocalizedDescription=The Internet connection appears to be offline.}
|
||||
// Fatal error: 'try!' expression unexpectedly raised an error: Search failed: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang_Fall2018/swiftlang_Fall2018-1000.11.42/src/swift/stdlib/public/core/ErrorType.swift, line 184
|
||||
// 2019-01-04 17:20:41.818432-0800 xctest[76410:1817499] Fatal error: 'try!' expression unexpectedly raised an error: Search failed: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang_Fall2018/swiftlang_Fall2018-1000.11.42/src/swift/stdlib/public/core/ErrorType.swift, line 184
|
||||
class MockURLSession: URLSession {
|
||||
private let responseFile: String
|
||||
|
||||
/// Initializes a mock URL session with a file for the response.
|
||||
///
|
||||
/// - Parameter responseFile: Name of file containing JSON response body.
|
||||
init(responseFile: String) {
|
||||
self.responseFile = responseFile
|
||||
}
|
||||
|
||||
/// Override which returns JSON contents from a file.
|
||||
///
|
||||
/// - Parameter requestString: Ignored URL string
|
||||
/// - Returns: Contents of responseFile
|
||||
@objc override func requestSynchronousJSONWithURLString(_ requestString: String) -> Any? {
|
||||
guard let fileURL = Bundle.jsonResponse(fileName: responseFile)
|
||||
else { fatalError("Unable to load file \(responseFile)") }
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
|
||||
if let jsonResult = jsonResult as? Dictionary<String, AnyObject> {
|
||||
return jsonResult
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing JSON: \(error)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Bundle {
|
||||
/// Locates a JSON response file from the test bundle.
|
||||
///
|
||||
/// - Parameter fileName: Name of file to locate.
|
||||
/// - Returns: URL to file.
|
||||
static func jsonResponse(fileName: String) -> URL? {
|
||||
return Bundle(for: MockURLSession.self).fileURL(fileName: fileName)
|
||||
}
|
||||
|
||||
/// Builds a URL for a file in the JSON directory of the current bundle.
|
||||
///
|
||||
/// - Parameter fileName: Name of file to locate.
|
||||
/// - Returns: URL to file.
|
||||
func fileURL(fileName: String) -> URL? {
|
||||
guard let path = self.path(forResource: fileName.fileNameWithoutExtension, ofType: fileName.fileExtension, inDirectory: "JSON")
|
||||
else { fatalError("Unable to load file \(fileName)") }
|
||||
|
||||
return URL(fileURLWithPath: path)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
/// Returns the file name before the extension.
|
||||
var fileNameWithoutExtension: String {
|
||||
return (self as NSString).deletingPathExtension
|
||||
}
|
||||
|
||||
/// Returns the file extension.
|
||||
var fileExtension: String {
|
||||
return (self as NSString).pathExtension
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MockSoftwareProduct.swift
|
||||
// SoftwareProductMock.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
|
@ -8,7 +8,7 @@
|
|||
|
||||
@testable import MasKit
|
||||
|
||||
struct MockSoftwareProduct: SoftwareProduct {
|
||||
struct SoftwareProductMock: SoftwareProduct {
|
||||
var appName: String
|
||||
var bundlePath: String
|
||||
var bundleVersion: String
|
78
MasKitTests/Network/NetworkManagerTests.swift
Normal file
78
MasKitTests/Network/NetworkManagerTests.swift
Normal file
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// NetworkManagerTests.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import MasKit
|
||||
import XCTest
|
||||
|
||||
class NetworkManagerTests: XCTestCase {
|
||||
func testSuccessfulAsyncResponse() {
|
||||
// Setup our objects
|
||||
let session = NetworkSessionMock()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
// Create data and tell the session to always return it
|
||||
let data = Data(bytes: [0, 1, 0, 1])
|
||||
session.data = data
|
||||
|
||||
// Create a URL (using the file path API to avoid optionals)
|
||||
let url = URL(fileURLWithPath: "url")
|
||||
|
||||
// Perform the request and verify the result
|
||||
var result: NetworkResult!
|
||||
manager.loadData(from: url) { result = $0 }
|
||||
XCTAssertEqual(result, NetworkResult.success(data))
|
||||
}
|
||||
|
||||
func testSuccessfulSyncResponse() {
|
||||
// Setup our objects
|
||||
let session = NetworkSessionMock()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
// Create data and tell the session to always return it
|
||||
let data = Data(bytes: [0, 1, 0, 1])
|
||||
session.data = data
|
||||
|
||||
// Create a URL (using the file path API to avoid optionals)
|
||||
let url = URL(fileURLWithPath: "url")
|
||||
|
||||
// Perform the request and verify the result
|
||||
let result = manager.loadDataSync(from: url)
|
||||
XCTAssertEqual(result, NetworkResult.success(data))
|
||||
}
|
||||
|
||||
func testFailureAsyncResponse() {
|
||||
// Setup our objects
|
||||
let session = NetworkSessionMock()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
session.error = NetworkManager.NetworkError.timeout
|
||||
|
||||
// Create a URL (using the file path API to avoid optionals)
|
||||
let url = URL(fileURLWithPath: "url")
|
||||
|
||||
// Perform the request and verify the result
|
||||
var result: NetworkResult!
|
||||
manager.loadData(from: url) { result = $0 }
|
||||
XCTAssertEqual(result, NetworkResult.failure(NetworkManager.NetworkError.timeout))
|
||||
}
|
||||
|
||||
func testFailureSyncResponse() {
|
||||
// Setup our objects
|
||||
let session = NetworkSessionMock()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
session.error = NetworkManager.NetworkError.timeout
|
||||
|
||||
// Create a URL (using the file path API to avoid optionals)
|
||||
let url = URL(fileURLWithPath: "url")
|
||||
|
||||
// Perform the request and verify the result
|
||||
let result = manager.loadDataSync(from: url)
|
||||
XCTAssertEqual(result, NetworkResult.failure(NetworkManager.NetworkError.timeout))
|
||||
}
|
||||
}
|
43
MasKitTests/Network/NetworkSessionMock.swift
Normal file
43
MasKitTests/Network/NetworkSessionMock.swift
Normal file
|
@ -0,0 +1,43 @@
|
|||
//
|
||||
// NetworkSessionMock
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 11/13/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import MasKit
|
||||
|
||||
/// Mock NetworkSession for testing.
|
||||
class NetworkSessionMock: NetworkSession {
|
||||
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
|
||||
|
||||
// Properties that enable us to set exactly what data or error
|
||||
// we want our mocked URLSession to return for any request.
|
||||
var data: Data?
|
||||
var error: Error?
|
||||
|
||||
/// Creates a mock data task
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: unused
|
||||
/// - completionHandler: Closure which is delivered both data and error properties (only one should be non-nil)
|
||||
/// - Returns: Mock data task
|
||||
func dataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> URLSessionDataTask {
|
||||
let data = self.data
|
||||
let error = self.error
|
||||
|
||||
return URLSessionDataTaskMock {
|
||||
completionHandler(data, nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Immediately passes data and error to completion handler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: unused
|
||||
/// - completionHandler: Closure which is delivered either data or an error.
|
||||
@objc func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
completionHandler(data, error)
|
||||
}
|
||||
}
|
40
MasKitTests/Network/NetworkSessionMockFromFile.swift
Normal file
40
MasKitTests/Network/NetworkSessionMockFromFile.swift
Normal file
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// NetworkSessionMockFromFile.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2019-01-05.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import MasKit
|
||||
|
||||
/// Mock NetworkSession for testing with saved JSON response payload files.
|
||||
class NetworkSessionMockFromFile: NetworkSessionMock {
|
||||
/// Path to response payload file relative to test bundle.
|
||||
private let responseFile: String
|
||||
|
||||
/// Initializes a mock URL session with a file for the response.
|
||||
///
|
||||
/// - Parameter responseFile: Name of file containing JSON response body.
|
||||
init(responseFile: String) {
|
||||
self.responseFile = responseFile
|
||||
}
|
||||
|
||||
/// Loads data from a file.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: unused
|
||||
/// - completionHandler: Closure which is delivered either data or an error.
|
||||
@objc override func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
guard let fileURL = Bundle.jsonResponse(fileName: responseFile)
|
||||
else { fatalError("Unable to load file \(responseFile)") }
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||
completionHandler(data, nil)
|
||||
} catch {
|
||||
print("Error opening file: \(error)")
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
}
|
38
MasKitTests/Network/TestURLSessionDelegate.swift
Normal file
38
MasKitTests/Network/TestURLSessionDelegate.swift
Normal file
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// TestURLSessionDelegate.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Delegate for network requests initiated from tests.
|
||||
class TestURLSessionDelegate: NSObject, URLSessionDelegate {
|
||||
func urlSession(_ session: URLSession,
|
||||
didReceive challenge: URLAuthenticationChallenge,
|
||||
completionHandler: (URLSession.AuthChallengeDisposition,
|
||||
URLCredential?) -> Void) {
|
||||
|
||||
// For example, you may want to override this to accept some self-signed certs here.
|
||||
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust &&
|
||||
Constants.selfSignedHosts.contains(challenge.protectionSpace.host) {
|
||||
// Allow the self-signed cert.
|
||||
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
|
||||
completionHandler(.useCredential, credential)
|
||||
} else {
|
||||
// You *have* to call completionHandler, so call
|
||||
// it to do the default action.
|
||||
completionHandler(.performDefaultHandling, nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct Constants {
|
||||
// A list of hosts you allow self-signed certificates on.
|
||||
// You'd likely have your dev/test servers here.
|
||||
// Please don't put your production server here!
|
||||
static let selfSignedHosts: Set<String> =
|
||||
["dev.example.com", "test.example.com"]
|
||||
}
|
||||
}
|
31
MasKitTests/Network/URLSessionConfiguration+Tests.swift
Normal file
31
MasKitTests/Network/URLSessionConfiguration+Tests.swift
Normal file
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// URLSessionConfiguration+Test.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Configuration for network requests initiated from tests.
|
||||
extension URLSessionConfiguration {
|
||||
/// Just like defaultSessionConfiguration, returns a
|
||||
/// newly created session configuration object, customised
|
||||
/// from the default to your requirements.
|
||||
class func testSessionConfiguration() -> URLSessionConfiguration {
|
||||
let config = self.default
|
||||
|
||||
// Eg we think 60s is too long a timeout time.
|
||||
config.timeoutIntervalForRequest = 20
|
||||
|
||||
// Some headers that are common to all reqeuests.
|
||||
// Eg my backend needs to be explicitly asked for JSON.
|
||||
config.httpAdditionalHeaders = ["MyResponseType": "JSON"]
|
||||
|
||||
// Eg we want to use pipelining.
|
||||
config.httpShouldUsePipelining = true
|
||||
|
||||
return config
|
||||
}
|
||||
}
|
24
MasKitTests/Network/URLSessionDataTaskMock.swift
Normal file
24
MasKitTests/Network/URLSessionDataTaskMock.swift
Normal file
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// URLSessionDataTaskMock .swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// Partial mock subclassing the original class
|
||||
class URLSessionDataTaskMock: URLSessionDataTask {
|
||||
private let closure: () -> Void
|
||||
|
||||
init(closure: @escaping () -> Void) {
|
||||
self.closure = closure
|
||||
}
|
||||
|
||||
// We override the 'resume' method and simply call our closure
|
||||
// instead of actually resuming any task.
|
||||
override func resume() {
|
||||
closure()
|
||||
}
|
||||
}
|
84
MasKitTests/OutputListener.swift
Normal file
84
MasKitTests/OutputListener.swift
Normal file
|
@ -0,0 +1,84 @@
|
|||
//
|
||||
// OutputListener.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/7/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Test helper for monitoring strings written to stdout. Modified from:
|
||||
/// https://medium.com/@thesaadismail/eavesdropping-on-swifts-print-statements-57f0215efb42
|
||||
class OutputListener {
|
||||
/// consumes the messages on STDOUT
|
||||
let inputPipe = Pipe()
|
||||
|
||||
/// outputs messages back to STDOUT
|
||||
let outputPipe = Pipe()
|
||||
|
||||
/// Buffers strings written to stdout
|
||||
var contents = ""
|
||||
|
||||
init() {
|
||||
// Set up a read handler which fires when data is written to our inputPipe
|
||||
inputPipe.fileHandleForReading.readabilityHandler = { [weak self] fileHandle in
|
||||
strongify(self) { context in
|
||||
let data = fileHandle.availableData
|
||||
if let string = String(data: data, encoding: String.Encoding.utf8) {
|
||||
context.contents += string
|
||||
}
|
||||
|
||||
// Write input back to stdout
|
||||
context.outputPipe.fileHandleForWriting.write(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OutputListener {
|
||||
/// Sets up the "tee" of piped output, intercepting stdout then passing it through.
|
||||
///
|
||||
/// ## [dup2 documentation](https://linux.die.net/man/2/dup2)
|
||||
/// `int dup2(int oldfd, int newfd);`
|
||||
/// `dup2()` makes `newfd` be the copy of `oldfd`, closing `newfd` first if necessary.
|
||||
func openConsolePipe() {
|
||||
var dupStatus: Int32
|
||||
|
||||
// Copy STDOUT file descriptor to outputPipe for writing strings back to STDOUT
|
||||
dupStatus = dup2(stdoutFileDescriptor, outputPipe.fileHandleForWriting.fileDescriptor)
|
||||
// Status should equal newfd
|
||||
assert(dupStatus == outputPipe.fileHandleForWriting.fileDescriptor)
|
||||
|
||||
// Intercept STDOUT with inputPipe
|
||||
// newFileDescriptor is the pipe's file descriptor and the old file descriptor is STDOUT_FILENO
|
||||
dupStatus = dup2(inputPipe.fileHandleForWriting.fileDescriptor, stdoutFileDescriptor)
|
||||
// Status should equal newfd
|
||||
assert(dupStatus == stdoutFileDescriptor)
|
||||
|
||||
// Don't have any tests on stderr yet
|
||||
// dup2(inputPipe.fileHandleForWriting.fileDescriptor, stderr)
|
||||
}
|
||||
|
||||
/// Tears down the "tee" of piped output.
|
||||
func closeConsolePipe() {
|
||||
// Restore stdout
|
||||
freopen("/dev/stdout", "a", stdout)
|
||||
|
||||
[inputPipe.fileHandleForReading, outputPipe.fileHandleForWriting].forEach { file in
|
||||
file.closeFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension OutputListener {
|
||||
/// File descriptor for stdout (aka STDOUT_FILENO)
|
||||
var stdoutFileDescriptor: Int32 {
|
||||
return FileHandle.standardOutput.fileDescriptor
|
||||
}
|
||||
|
||||
/// File descriptor for stderr (aka STDERR_FILENO)
|
||||
var stderrFileDescriptor: Int32 {
|
||||
return FileHandle.standardError.fileDescriptor
|
||||
}
|
||||
}
|
48
MasKitTests/OutputListenerSpec.swift
Normal file
48
MasKitTests/OutputListenerSpec.swift
Normal file
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// OutputListenerSpec.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/8/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Quick
|
||||
import Nimble
|
||||
|
||||
class OutputListenerSpec: QuickSpec {
|
||||
override func spec() {
|
||||
xdescribe("output listener") {
|
||||
it("can intercept a single line written stdout") {
|
||||
let output = OutputListener()
|
||||
output.openConsolePipe()
|
||||
|
||||
let expectedOutput = "hi there"
|
||||
|
||||
print("hi there", terminator: "")
|
||||
|
||||
// output is async so need to wait for contents to be updated
|
||||
expect(output.contents).toEventuallyNot(beEmpty())
|
||||
expect(output.contents) == expectedOutput
|
||||
|
||||
output.closeConsolePipe()
|
||||
}
|
||||
it("can intercept multiple lines written stdout") {
|
||||
let output = OutputListener()
|
||||
output.openConsolePipe()
|
||||
|
||||
let expectedOutput = """
|
||||
hi there
|
||||
|
||||
"""
|
||||
|
||||
print("hi there")
|
||||
|
||||
// output is async so need to wait for contents to be updated
|
||||
expect(output.contents).toEventuallyNot(beEmpty())
|
||||
expect(output.contents) == expectedOutput
|
||||
|
||||
output.closeConsolePipe()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
MasKitTests/Strongify.swift
Normal file
22
MasKitTests/Strongify.swift
Normal file
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// Strongify.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/8/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
// https://medium.com/@merowing_/stop-weak-strong-dance-in-swift-3aec6d3563d4
|
||||
|
||||
func strongify<Context: AnyObject, Arguments>(_ context: Context?,
|
||||
closure: @escaping (Context, Arguments) -> Void) -> (Arguments) -> Void {
|
||||
return { [weak context] arguments in
|
||||
guard let strongContext = context else { return }
|
||||
closure(strongContext, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
func strongify<Context: AnyObject>(_ context: Context?, closure: @escaping (Context) -> Void) {
|
||||
guard let strongContext = context else { return }
|
||||
closure(strongContext)
|
||||
}
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
A simple command line interface for the Mac App Store. Designed for scripting and automation.
|
||||
|
||||
[![Software License](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/mas-cli/mas/blob/master/LICENSE)
|
||||
[![Swift 4.2](https://img.shields.io/badge/Language-Swift_4.2-orange.svg)](https://swift.org)
|
||||
[![GitHub Release](https://img.shields.io/github/release/mas-cli/mas.svg)](https://github.com/mas-cli/mas/releases)
|
||||
[![Reviewed by Hound](https://img.shields.io/badge/Reviewed_by-Hound-8E64B0.svg)](https://houndci.com)
|
||||
[![Pipeline Status](https://jenkins.log-g.co/buildStatus/icon?job=mas-cli/mas/master)](https://jenkins.log-g.co/job/mas-cli/job/mas/job/master/)
|
||||
|
||||
## 📲 Install
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
B537017621A0F94200538F78 /* Commandant.framework in Copy Carthage Frameworks */ = {isa = PBXBuildFile; fileRef = 90CB406B213F4DDD0044E445 /* Commandant.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
B5552928219A1BB900ACB4CA /* CommerceKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F83213A62173EF75008BA8A0 /* CommerceKit.framework */; };
|
||||
B5552929219A1BC700ACB4CA /* StoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F83213A52173EF75008BA8A0 /* StoreFoundation.framework */; };
|
||||
B555292D219A1FE700ACB4CA /* SearchSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B555292C219A1FE700ACB4CA /* SearchSpec.swift */; };
|
||||
B555292E219A218E00ACB4CA /* Quick.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90CB406A213F4DDD0044E445 /* Quick.framework */; };
|
||||
B555292F219A219100ACB4CA /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90CB406C213F4DDD0044E445 /* Nimble.framework */; };
|
||||
B5552936219A23FF00ACB4CA /* Nimble.framework in Copy Carthage Frameworks */ = {isa = PBXBuildFile; fileRef = 90CB406C213F4DDD0044E445 /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -21,10 +20,29 @@
|
|||
B576FDF321E03B780016B39D /* MasStoreSearchSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FDF221E03B780016B39D /* MasStoreSearchSpec.swift */; };
|
||||
B576FDF521E1078F0016B39D /* MASErrorTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FDF421E1078F0016B39D /* MASErrorTestCase.swift */; };
|
||||
B576FDF721E107AA0016B39D /* OpenSystemCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FDF621E107AA0016B39D /* OpenSystemCommand.swift */; };
|
||||
B576FDF921E107CA0016B39D /* MockSoftwareProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FDF821E107CA0016B39D /* MockSoftwareProduct.swift */; };
|
||||
B576FDF921E107CA0016B39D /* SoftwareProductMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FDF821E107CA0016B39D /* SoftwareProductMock.swift */; };
|
||||
B576FDFA21E1081C0016B39D /* SearchResultList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B15521D89F5200F3AC59 /* SearchResultList.swift */; };
|
||||
B576FDFC21E10A610016B39D /* URLSessionConfiguration+Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FDFB21E10A610016B39D /* URLSessionConfiguration+Tests.swift */; };
|
||||
B576FDFE21E10B660016B39D /* TestURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FDFD21E10B660016B39D /* TestURLSessionDelegate.swift */; };
|
||||
B576FE0021E113610016B39D /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FDFF21E113610016B39D /* NetworkSession.swift */; };
|
||||
B576FE0221E1139E0016B39D /* URLSession+NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE0121E1139E0016B39D /* URLSession+NetworkSession.swift */; };
|
||||
B576FE0421E113E90016B39D /* NetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE0321E113E90016B39D /* NetworkManager.swift */; };
|
||||
B576FE0821E114A80016B39D /* NetworkResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE0721E114A80016B39D /* NetworkResult.swift */; };
|
||||
B576FE0C21E116590016B39D /* NetworkManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE0B21E116590016B39D /* NetworkManagerTests.swift */; };
|
||||
B576FE0E21E1D6310016B39D /* String+PercentEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE0D21E1D6310016B39D /* String+PercentEncoding.swift */; };
|
||||
B576FE1221E1D82D0016B39D /* NetworkSessionMockFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE1121E1D82D0016B39D /* NetworkSessionMockFromFile.swift */; };
|
||||
B576FE1421E1D8A90016B39D /* Bundle+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE1321E1D8A90016B39D /* Bundle+JSON.swift */; };
|
||||
B576FE1621E1D8CB0016B39D /* String+FileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE1521E1D8CB0016B39D /* String+FileExtension.swift */; };
|
||||
B576FE1B21E28E8A0016B39D /* NetworkSessionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE1A21E28E8A0016B39D /* NetworkSessionMock.swift */; };
|
||||
B576FE1D21E28EF70016B39D /* URLSessionDataTaskMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE1C21E28EF70016B39D /* URLSessionDataTaskMock.swift */; };
|
||||
B576FE2821E423E60016B39D /* Dictionary+StringOrEmpty.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE2721E423E60016B39D /* Dictionary+StringOrEmpty.swift */; };
|
||||
B576FE2A21E4240B0016B39D /* AppInfoFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE2921E4240B0016B39D /* AppInfoFormatter.swift */; };
|
||||
B576FE2C21E42A230016B39D /* OutputListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE2B21E42A230016B39D /* OutputListener.swift */; };
|
||||
B576FE2E21E5A8010016B39D /* Strongify.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE2D21E5A8010016B39D /* Strongify.swift */; };
|
||||
B576FE3021E5BD130016B39D /* OutputListenerSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE2F21E5BD130016B39D /* OutputListenerSpec.swift */; };
|
||||
B576FE3321E985250016B39D /* SearchResultFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE3221E985250016B39D /* SearchResultFormatter.swift */; };
|
||||
B576FE3521E98AAE0016B39D /* StoreSearchSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE3421E98AAE0016B39D /* StoreSearchSpec.swift */; };
|
||||
B5793E29219BDD4800135B39 /* JSON in Resources */ = {isa = PBXBuildFile; fileRef = B5793E28219BDD4800135B39 /* JSON */; };
|
||||
B5793E2B219BE0CD00135B39 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5793E2A219BE0CD00135B39 /* MockURLSession.swift */; };
|
||||
B588CE0221DC89490047D305 /* ExternalCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = B588CE0121DC89490047D305 /* ExternalCommand.swift */; };
|
||||
B588CE0421DC8AFB0047D305 /* TrashCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = B588CE0321DC8AFB0047D305 /* TrashCommand.swift */; };
|
||||
B594B12021D53A8200F3AC59 /* Uninstall.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B11F21D53A8200F3AC59 /* Uninstall.swift */; };
|
||||
|
@ -33,7 +51,7 @@
|
|||
B594B12721D5825800F3AC59 /* AppLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12621D5825800F3AC59 /* AppLibrary.swift */; };
|
||||
B594B12921D5831D00F3AC59 /* SoftwareProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12821D5831D00F3AC59 /* SoftwareProduct.swift */; };
|
||||
B594B12B21D5837200F3AC59 /* CKSoftwareProduct+SoftwareProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12A21D5837200F3AC59 /* CKSoftwareProduct+SoftwareProduct.swift */; };
|
||||
B594B12E21D5850700F3AC59 /* MockAppLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12D21D5850700F3AC59 /* MockAppLibrary.swift */; };
|
||||
B594B12E21D5850700F3AC59 /* AppLibraryMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12D21D5850700F3AC59 /* AppLibraryMock.swift */; };
|
||||
B594B13021D5855D00F3AC59 /* MasAppLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12F21D5855D00F3AC59 /* MasAppLibrary.swift */; };
|
||||
B594B13221D5876200F3AC59 /* ResultPredicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B13121D5876200F3AC59 /* ResultPredicates.swift */; };
|
||||
B594B13621D6D68600F3AC59 /* VersionCommandSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B13521D6D68600F3AC59 /* VersionCommandSpec.swift */; };
|
||||
|
@ -56,8 +74,8 @@
|
|||
B5DBF80F21DEEB7B00F3B151 /* Vendor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF80E21DEEB7B00F3B151 /* Vendor.swift */; };
|
||||
B5DBF81121DEEC4200F3B151 /* VendorCommandSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF81021DEEC4200F3B151 /* VendorCommandSpec.swift */; };
|
||||
B5DBF81321DEEC7C00F3B151 /* OpenCommandSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF81221DEEC7C00F3B151 /* OpenCommandSpec.swift */; };
|
||||
B5DBF81521E02BA900F3B151 /* MockStoreSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF81421E02BA900F3B151 /* MockStoreSearch.swift */; };
|
||||
B5DBF81721E02E3400F3B151 /* MockOpenSystemCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF81621E02E3400F3B151 /* MockOpenSystemCommand.swift */; };
|
||||
B5DBF81521E02BA900F3B151 /* StoreSearchMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF81421E02BA900F3B151 /* StoreSearchMock.swift */; };
|
||||
B5DBF81721E02E3400F3B151 /* OpenSystemCommandMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF81621E02E3400F3B151 /* OpenSystemCommandMock.swift */; };
|
||||
ED031A7C1B5127C00097692E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED031A7B1B5127C00097692E /* main.swift */; };
|
||||
F83213892173D3E1008BA8A0 /* CKAccountStore.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB719B20F2EC4500F56FDC /* CKAccountStore.h */; };
|
||||
F832138A2173D3E1008BA8A0 /* CKDownloadQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB719C20F2EC4500F56FDC /* CKDownloadQueue.h */; };
|
||||
|
@ -109,7 +127,6 @@
|
|||
F8FB717920F2B4DD00F56FDC /* Upgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD3B3621C34709400B56B88 /* Upgrade.swift */; };
|
||||
F8FB717A20F2B4DD00F56FDC /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB6CE8B1BAEC3D400648B4D /* Version.swift */; };
|
||||
F8FB717B20F2B4DD00F56FDC /* MASError.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F238C1B8756E600AE40CD /* MASError.swift */; };
|
||||
F8FB717C20F2B4DD00F56FDC /* NSURLSession+Synchronous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A989A1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift */; };
|
||||
F8FB717D20F2B4DD00F56FDC /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDCBF9541D89CFC7000039C6 /* Utilities.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -180,20 +197,37 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
693A98981CBFFA760004D3B4 /* Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
|
||||
693A989A1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSURLSession+Synchronous.swift"; sourceTree = "<group>"; };
|
||||
8078FAA71EC4F2FB004B5B3F /* Lucky.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lucky.swift; sourceTree = "<group>"; };
|
||||
900A1E801DBAC8CB0069B1A8 /* Info.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Info.swift; sourceTree = "<group>"; };
|
||||
90CB4069213F4DDD0044E445 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = "<group>"; };
|
||||
90CB406A213F4DDD0044E445 /* Quick.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Quick.framework; sourceTree = "<group>"; };
|
||||
90CB406B213F4DDD0044E445 /* Commandant.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Commandant.framework; sourceTree = "<group>"; };
|
||||
90CB406C213F4DDD0044E445 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Nimble.framework; sourceTree = "<group>"; };
|
||||
B555292C219A1FE700ACB4CA /* SearchSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSpec.swift; sourceTree = "<group>"; };
|
||||
B576FDF221E03B780016B39D /* MasStoreSearchSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasStoreSearchSpec.swift; sourceTree = "<group>"; };
|
||||
B576FDF421E1078F0016B39D /* MASErrorTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MASErrorTestCase.swift; sourceTree = "<group>"; };
|
||||
B576FDF621E107AA0016B39D /* OpenSystemCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenSystemCommand.swift; sourceTree = "<group>"; };
|
||||
B576FDF821E107CA0016B39D /* MockSoftwareProduct.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockSoftwareProduct.swift; sourceTree = "<group>"; };
|
||||
B576FDF821E107CA0016B39D /* SoftwareProductMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SoftwareProductMock.swift; sourceTree = "<group>"; };
|
||||
B576FDFB21E10A610016B39D /* URLSessionConfiguration+Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSessionConfiguration+Tests.swift"; sourceTree = "<group>"; };
|
||||
B576FDFD21E10B660016B39D /* TestURLSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestURLSessionDelegate.swift; sourceTree = "<group>"; };
|
||||
B576FDFF21E113610016B39D /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = "<group>"; };
|
||||
B576FE0121E1139E0016B39D /* URLSession+NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+NetworkSession.swift"; sourceTree = "<group>"; };
|
||||
B576FE0321E113E90016B39D /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = "<group>"; };
|
||||
B576FE0721E114A80016B39D /* NetworkResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResult.swift; sourceTree = "<group>"; };
|
||||
B576FE0B21E116590016B39D /* NetworkManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerTests.swift; sourceTree = "<group>"; };
|
||||
B576FE0D21E1D6310016B39D /* String+PercentEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+PercentEncoding.swift"; sourceTree = "<group>"; };
|
||||
B576FE1121E1D82D0016B39D /* NetworkSessionMockFromFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkSessionMockFromFile.swift; sourceTree = "<group>"; };
|
||||
B576FE1321E1D8A90016B39D /* Bundle+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+JSON.swift"; sourceTree = "<group>"; };
|
||||
B576FE1521E1D8CB0016B39D /* String+FileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FileExtension.swift"; sourceTree = "<group>"; };
|
||||
B576FE1A21E28E8A0016B39D /* NetworkSessionMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkSessionMock.swift; sourceTree = "<group>"; };
|
||||
B576FE1C21E28EF70016B39D /* URLSessionDataTaskMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionDataTaskMock.swift; sourceTree = "<group>"; };
|
||||
B576FE2721E423E60016B39D /* Dictionary+StringOrEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+StringOrEmpty.swift"; sourceTree = "<group>"; };
|
||||
B576FE2921E4240B0016B39D /* AppInfoFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoFormatter.swift; sourceTree = "<group>"; };
|
||||
B576FE2B21E42A230016B39D /* OutputListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputListener.swift; sourceTree = "<group>"; };
|
||||
B576FE2D21E5A8010016B39D /* Strongify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strongify.swift; sourceTree = "<group>"; };
|
||||
B576FE2F21E5BD130016B39D /* OutputListenerSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputListenerSpec.swift; sourceTree = "<group>"; };
|
||||
B576FE3221E985250016B39D /* SearchResultFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultFormatter.swift; sourceTree = "<group>"; };
|
||||
B576FE3421E98AAE0016B39D /* StoreSearchSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreSearchSpec.swift; sourceTree = "<group>"; };
|
||||
B5793E28219BDD4800135B39 /* JSON */ = {isa = PBXFileReference; lastKnownFileType = folder; path = JSON; sourceTree = "<group>"; };
|
||||
B5793E2A219BE0CD00135B39 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = "<group>"; };
|
||||
B588CE0121DC89490047D305 /* ExternalCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalCommand.swift; sourceTree = "<group>"; };
|
||||
B588CE0321DC8AFB0047D305 /* TrashCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrashCommand.swift; sourceTree = "<group>"; };
|
||||
B594B11F21D53A8200F3AC59 /* Uninstall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Uninstall.swift; sourceTree = "<group>"; };
|
||||
|
@ -202,7 +236,7 @@
|
|||
B594B12621D5825800F3AC59 /* AppLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLibrary.swift; sourceTree = "<group>"; };
|
||||
B594B12821D5831D00F3AC59 /* SoftwareProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareProduct.swift; sourceTree = "<group>"; };
|
||||
B594B12A21D5837200F3AC59 /* CKSoftwareProduct+SoftwareProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKSoftwareProduct+SoftwareProduct.swift"; sourceTree = "<group>"; };
|
||||
B594B12D21D5850700F3AC59 /* MockAppLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppLibrary.swift; sourceTree = "<group>"; };
|
||||
B594B12D21D5850700F3AC59 /* AppLibraryMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLibraryMock.swift; sourceTree = "<group>"; };
|
||||
B594B12F21D5855D00F3AC59 /* MasAppLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasAppLibrary.swift; sourceTree = "<group>"; };
|
||||
B594B13121D5876200F3AC59 /* ResultPredicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultPredicates.swift; sourceTree = "<group>"; };
|
||||
B594B13521D6D68600F3AC59 /* VersionCommandSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionCommandSpec.swift; sourceTree = "<group>"; };
|
||||
|
@ -226,8 +260,8 @@
|
|||
B5DBF80E21DEEB7B00F3B151 /* Vendor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Vendor.swift; sourceTree = "<group>"; };
|
||||
B5DBF81021DEEC4200F3B151 /* VendorCommandSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VendorCommandSpec.swift; sourceTree = "<group>"; };
|
||||
B5DBF81221DEEC7C00F3B151 /* OpenCommandSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenCommandSpec.swift; sourceTree = "<group>"; };
|
||||
B5DBF81421E02BA900F3B151 /* MockStoreSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockStoreSearch.swift; sourceTree = "<group>"; };
|
||||
B5DBF81621E02E3400F3B151 /* MockOpenSystemCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOpenSystemCommand.swift; sourceTree = "<group>"; };
|
||||
B5DBF81421E02BA900F3B151 /* StoreSearchMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreSearchMock.swift; sourceTree = "<group>"; };
|
||||
B5DBF81621E02E3400F3B151 /* OpenSystemCommandMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSystemCommandMock.swift; sourceTree = "<group>"; };
|
||||
ED031A781B5127C00097692E /* mas */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = mas; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
ED031A7B1B5127C00097692E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
ED0F237E1B87522400AE40CD /* Install.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Install.swift; sourceTree = "<group>"; };
|
||||
|
@ -331,6 +365,139 @@
|
|||
path = Carthage/Build/Mac;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE0921E114BD0016B39D /* Network */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B576FE0321E113E90016B39D /* NetworkManager.swift */,
|
||||
B576FE0721E114A80016B39D /* NetworkResult.swift */,
|
||||
B576FDFF21E113610016B39D /* NetworkSession.swift */,
|
||||
B576FE0121E1139E0016B39D /* URLSession+NetworkSession.swift */,
|
||||
);
|
||||
path = Network;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE0A21E116470016B39D /* Network */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B576FE0B21E116590016B39D /* NetworkManagerTests.swift */,
|
||||
B576FE1A21E28E8A0016B39D /* NetworkSessionMock.swift */,
|
||||
B576FE1121E1D82D0016B39D /* NetworkSessionMockFromFile.swift */,
|
||||
B576FDFD21E10B660016B39D /* TestURLSessionDelegate.swift */,
|
||||
B576FDFB21E10A610016B39D /* URLSessionConfiguration+Tests.swift */,
|
||||
B576FE1C21E28EF70016B39D /* URLSessionDataTaskMock.swift */,
|
||||
);
|
||||
path = Network;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE1721E28E1F0016B39D /* ExternalCommands */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B5DBF81621E02E3400F3B151 /* OpenSystemCommandMock.swift */,
|
||||
);
|
||||
path = ExternalCommands;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE1821E28E460016B39D /* Nimble */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B594B13121D5876200F3AC59 /* ResultPredicates.swift */,
|
||||
);
|
||||
path = Nimble;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE1921E28E530016B39D /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B576FE1321E1D8A90016B39D /* Bundle+JSON.swift */,
|
||||
B576FE1521E1D8CB0016B39D /* String+FileExtension.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE1E21E2904E0016B39D /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B594B15321D89DF400F3AC59 /* SearchResult.swift */,
|
||||
B594B15521D89F5200F3AC59 /* SearchResultList.swift */,
|
||||
B594B12821D5831D00F3AC59 /* SoftwareProduct.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE1F21E290720016B39D /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B576FE2721E423E60016B39D /* Dictionary+StringOrEmpty.swift */,
|
||||
B576FE0D21E1D6310016B39D /* String+PercentEncoding.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE2021E2908D0016B39D /* Formatters */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B576FE2921E4240B0016B39D /* AppInfoFormatter.swift */,
|
||||
B576FE3221E985250016B39D /* SearchResultFormatter.swift */,
|
||||
EDCBF9541D89CFC7000039C6 /* Utilities.swift */,
|
||||
);
|
||||
path = Formatters;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE2121E292F80016B39D /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B594B12621D5825800F3AC59 /* AppLibrary.swift */,
|
||||
B594B12F21D5855D00F3AC59 /* MasAppLibrary.swift */,
|
||||
B594B15121D89A8B00F3AC59 /* MasStoreSearch.swift */,
|
||||
B594B14F21D8998000F3AC59 /* StoreSearch.swift */,
|
||||
);
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE2221E2932B0016B39D /* Controllers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B594B12D21D5850700F3AC59 /* AppLibraryMock.swift */,
|
||||
B576FDF221E03B780016B39D /* MasStoreSearchSpec.swift */,
|
||||
B5DBF81421E02BA900F3B151 /* StoreSearchMock.swift */,
|
||||
B576FE3421E98AAE0016B39D /* StoreSearchSpec.swift */,
|
||||
);
|
||||
path = Controllers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE2321E29CAC0016B39D /* Errors */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED0F238C1B8756E600AE40CD /* MASError.swift */,
|
||||
);
|
||||
path = Errors;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE2421E29CE80016B39D /* Errors */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B576FDF421E1078F0016B39D /* MASErrorTestCase.swift */,
|
||||
);
|
||||
path = Errors;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE2521E29D2D0016B39D /* SupportingFiles */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F8FB715520F2B41400F56FDC /* Info.plist */,
|
||||
F8FB715420F2B41400F56FDC /* MasKit.h */,
|
||||
);
|
||||
path = SupportingFiles;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B576FE2621E29DD90016B39D /* SupportingFiles */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F8FB716120F2B41400F56FDC /* Info.plist */,
|
||||
);
|
||||
path = SupportingFiles;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B588CE0021DC89250047D305 /* ExternalCommands */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -354,7 +521,6 @@
|
|||
B594B14121D6D8EC00F3AC59 /* OutdatedCommandSpec.swift */,
|
||||
B594B13F21D6D8BF00F3AC59 /* ResetCommandSpec.swift */,
|
||||
B594B13D21D6D78900F3AC59 /* SearchCommandSpec.swift */,
|
||||
B555292C219A1FE700ACB4CA /* SearchSpec.swift */,
|
||||
B594B13B21D6D72E00F3AC59 /* SignInCommandSpec.swift */,
|
||||
B594B13921D6D70400F3AC59 /* SignOutCommandSpec.swift */,
|
||||
B594B12421D580BB00F3AC59 /* UninstallCommandSpec.swift */,
|
||||
|
@ -365,16 +531,12 @@
|
|||
path = Commands;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B594B12C21D584E800F3AC59 /* Mocks */ = {
|
||||
B594B12C21D584E800F3AC59 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B594B12D21D5850700F3AC59 /* MockAppLibrary.swift */,
|
||||
B5DBF81621E02E3400F3B151 /* MockOpenSystemCommand.swift */,
|
||||
B576FDF821E107CA0016B39D /* MockSoftwareProduct.swift */,
|
||||
B5DBF81421E02BA900F3B151 /* MockStoreSearch.swift */,
|
||||
B5793E2A219BE0CD00135B39 /* MockURLSession.swift */,
|
||||
B576FDF821E107CA0016B39D /* SoftwareProductMock.swift */,
|
||||
);
|
||||
path = Mocks;
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
ED031A6F1B5127C00097692E = {
|
||||
|
@ -460,19 +622,14 @@
|
|||
children = (
|
||||
ED0F238E1B87A54700AE40CD /* AppStore */,
|
||||
ED0F23801B87524700AE40CD /* Commands */,
|
||||
B576FE2121E292F80016B39D /* Controllers */,
|
||||
B576FE2321E29CAC0016B39D /* Errors */,
|
||||
B576FE1F21E290720016B39D /* Extensions */,
|
||||
B588CE0021DC89250047D305 /* ExternalCommands */,
|
||||
B594B12621D5825800F3AC59 /* AppLibrary.swift */,
|
||||
F8FB715520F2B41400F56FDC /* Info.plist */,
|
||||
B594B12F21D5855D00F3AC59 /* MasAppLibrary.swift */,
|
||||
ED0F238C1B8756E600AE40CD /* MASError.swift */,
|
||||
F8FB715420F2B41400F56FDC /* MasKit.h */,
|
||||
B594B15121D89A8B00F3AC59 /* MasStoreSearch.swift */,
|
||||
693A989A1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift */,
|
||||
B594B15321D89DF400F3AC59 /* SearchResult.swift */,
|
||||
B594B15521D89F5200F3AC59 /* SearchResultList.swift */,
|
||||
B594B12821D5831D00F3AC59 /* SoftwareProduct.swift */,
|
||||
B594B14F21D8998000F3AC59 /* StoreSearch.swift */,
|
||||
EDCBF9541D89CFC7000039C6 /* Utilities.swift */,
|
||||
B576FE2021E2908D0016B39D /* Formatters */,
|
||||
B576FE1E21E2904E0016B39D /* Models */,
|
||||
B576FE0921E114BD0016B39D /* Network */,
|
||||
B576FE2521E29D2D0016B39D /* SupportingFiles */,
|
||||
);
|
||||
path = MasKit;
|
||||
sourceTree = "<group>";
|
||||
|
@ -481,12 +638,18 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
B594B12321D57FF300F3AC59 /* Commands */,
|
||||
F8FB716120F2B41400F56FDC /* Info.plist */,
|
||||
B576FE2221E2932B0016B39D /* Controllers */,
|
||||
B576FE2421E29CE80016B39D /* Errors */,
|
||||
B576FE1921E28E530016B39D /* Extensions */,
|
||||
B576FE1721E28E1F0016B39D /* ExternalCommands */,
|
||||
B5793E28219BDD4800135B39 /* JSON */,
|
||||
B576FDF421E1078F0016B39D /* MASErrorTestCase.swift */,
|
||||
B576FDF221E03B780016B39D /* MasStoreSearchSpec.swift */,
|
||||
B594B12C21D584E800F3AC59 /* Mocks */,
|
||||
B594B13121D5876200F3AC59 /* ResultPredicates.swift */,
|
||||
B594B12C21D584E800F3AC59 /* Models */,
|
||||
B576FE0A21E116470016B39D /* Network */,
|
||||
B576FE1821E28E460016B39D /* Nimble */,
|
||||
B576FE2B21E42A230016B39D /* OutputListener.swift */,
|
||||
B576FE2F21E5BD130016B39D /* OutputListenerSpec.swift */,
|
||||
B576FE2D21E5A8010016B39D /* Strongify.swift */,
|
||||
B576FE2621E29DD90016B39D /* SupportingFiles */,
|
||||
);
|
||||
path = MasKitTests;
|
||||
sourceTree = "<group>";
|
||||
|
@ -609,6 +772,7 @@
|
|||
F8FB714F20F2B41400F56FDC /* Frameworks */,
|
||||
F83213A72173F58B008BA8A0 /* Copy Frameworks */,
|
||||
F8FB715020F2B41400F56FDC /* Resources */,
|
||||
B576FE3121E96E6C0016B39D /* SwiftLint */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
|
@ -708,6 +872,27 @@
|
|||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
B576FE3121E96E6C0016B39D /* SwiftLint */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = SwiftLint;
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "if which -s swiftlint; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
ED031A741B5127C00097692E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
|
@ -733,26 +918,33 @@
|
|||
F8FB717220F2B4DD00F56FDC /* List.swift in Sources */,
|
||||
F8FB717320F2B4DD00F56FDC /* Lucky.swift in Sources */,
|
||||
B594B13021D5855D00F3AC59 /* MasAppLibrary.swift in Sources */,
|
||||
B576FDF721E107AA0016B39D /* OpenSystemCommand.swift in Sources */,
|
||||
F8FB717B20F2B4DD00F56FDC /* MASError.swift in Sources */,
|
||||
B594B15221D89A8B00F3AC59 /* MasStoreSearch.swift in Sources */,
|
||||
F8FB717C20F2B4DD00F56FDC /* NSURLSession+Synchronous.swift in Sources */,
|
||||
B576FE0421E113E90016B39D /* NetworkManager.swift in Sources */,
|
||||
B576FE0821E114A80016B39D /* NetworkResult.swift in Sources */,
|
||||
B576FE0021E113610016B39D /* NetworkSession.swift in Sources */,
|
||||
B5DBF80D21DEE4E600F3B151 /* Open.swift in Sources */,
|
||||
B576FDF721E107AA0016B39D /* OpenSystemCommand.swift in Sources */,
|
||||
F8FB717420F2B4DD00F56FDC /* Outdated.swift in Sources */,
|
||||
B576FE2A21E4240B0016B39D /* AppInfoFormatter.swift in Sources */,
|
||||
F8FB716C20F2B4DD00F56FDC /* PurchaseDownloadObserver.swift in Sources */,
|
||||
F8FB717520F2B4DD00F56FDC /* Reset.swift in Sources */,
|
||||
F8FB717620F2B4DD00F56FDC /* Search.swift in Sources */,
|
||||
B594B15421D89DF400F3AC59 /* SearchResult.swift in Sources */,
|
||||
B576FDFA21E1081C0016B39D /* SearchResultList.swift in Sources */,
|
||||
B576FE2821E423E60016B39D /* Dictionary+StringOrEmpty.swift in Sources */,
|
||||
F8FB717720F2B4DD00F56FDC /* SignIn.swift in Sources */,
|
||||
F8FB717820F2B4DD00F56FDC /* SignOut.swift in Sources */,
|
||||
B576FE3321E985250016B39D /* SearchResultFormatter.swift in Sources */,
|
||||
B594B12921D5831D00F3AC59 /* SoftwareProduct.swift in Sources */,
|
||||
F8FB716D20F2B4DD00F56FDC /* SSPurchase.swift in Sources */,
|
||||
F8FB716E20F2B4DD00F56FDC /* StoreAccount.swift in Sources */,
|
||||
B594B15021D8998000F3AC59 /* StoreSearch.swift in Sources */,
|
||||
B576FE0E21E1D6310016B39D /* String+PercentEncoding.swift in Sources */,
|
||||
B588CE0421DC8AFB0047D305 /* TrashCommand.swift in Sources */,
|
||||
B594B12021D53A8200F3AC59 /* Uninstall.swift in Sources */,
|
||||
F8FB717920F2B4DD00F56FDC /* Upgrade.swift in Sources */,
|
||||
B576FE0221E1139E0016B39D /* URLSession+NetworkSession.swift in Sources */,
|
||||
F8FB717D20F2B4DD00F56FDC /* Utilities.swift in Sources */,
|
||||
B5DBF80F21DEEB7B00F3B151 /* Vendor.swift in Sources */,
|
||||
F8FB717A20F2B4DD00F56FDC /* Version.swift in Sources */,
|
||||
|
@ -764,29 +956,39 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B594B14A21D6D9AE00F3AC59 /* AccountCommandSpec.swift in Sources */,
|
||||
B594B12E21D5850700F3AC59 /* AppLibraryMock.swift in Sources */,
|
||||
B576FE1421E1D8A90016B39D /* Bundle+JSON.swift in Sources */,
|
||||
B594B14E21D8984500F3AC59 /* HomeCommandSpec.swift in Sources */,
|
||||
B594B14821D6D98400F3AC59 /* InfoCommandSpec.swift in Sources */,
|
||||
B594B14621D6D95700F3AC59 /* InstallCommandSpec.swift in Sources */,
|
||||
B594B12221D5416100F3AC59 /* ListCommandSpec.swift in Sources */,
|
||||
B594B14421D6D91800F3AC59 /* LuckyCommandSpec.swift in Sources */,
|
||||
B576FDF521E1078F0016B39D /* MASErrorTestCase.swift in Sources */,
|
||||
B576FDF321E03B780016B39D /* MasStoreSearchSpec.swift in Sources */,
|
||||
B594B12E21D5850700F3AC59 /* MockAppLibrary.swift in Sources */,
|
||||
B5DBF81721E02E3400F3B151 /* MockOpenSystemCommand.swift in Sources */,
|
||||
B5DBF81521E02BA900F3B151 /* MockStoreSearch.swift in Sources */,
|
||||
B5793E2B219BE0CD00135B39 /* MockURLSession.swift in Sources */,
|
||||
B576FE0C21E116590016B39D /* NetworkManagerTests.swift in Sources */,
|
||||
B576FE1B21E28E8A0016B39D /* NetworkSessionMock.swift in Sources */,
|
||||
B576FE2C21E42A230016B39D /* OutputListener.swift in Sources */,
|
||||
B576FE1221E1D82D0016B39D /* NetworkSessionMockFromFile.swift in Sources */,
|
||||
B5DBF81321DEEC7C00F3B151 /* OpenCommandSpec.swift in Sources */,
|
||||
B5DBF81721E02E3400F3B151 /* OpenSystemCommandMock.swift in Sources */,
|
||||
B594B14221D6D8EC00F3AC59 /* OutdatedCommandSpec.swift in Sources */,
|
||||
B594B14021D6D8BF00F3AC59 /* ResetCommandSpec.swift in Sources */,
|
||||
B576FDF921E107CA0016B39D /* MockSoftwareProduct.swift in Sources */,
|
||||
B594B13221D5876200F3AC59 /* ResultPredicates.swift in Sources */,
|
||||
B594B13E21D6D78900F3AC59 /* SearchCommandSpec.swift in Sources */,
|
||||
B555292D219A1FE700ACB4CA /* SearchSpec.swift in Sources */,
|
||||
B594B13C21D6D72E00F3AC59 /* SignInCommandSpec.swift in Sources */,
|
||||
B594B13A21D6D70400F3AC59 /* SignOutCommandSpec.swift in Sources */,
|
||||
B576FE3021E5BD130016B39D /* OutputListenerSpec.swift in Sources */,
|
||||
B576FE3521E98AAE0016B39D /* StoreSearchSpec.swift in Sources */,
|
||||
B576FDF921E107CA0016B39D /* SoftwareProductMock.swift in Sources */,
|
||||
B5DBF81521E02BA900F3B151 /* StoreSearchMock.swift in Sources */,
|
||||
B576FE1621E1D8CB0016B39D /* String+FileExtension.swift in Sources */,
|
||||
B576FDFE21E10B660016B39D /* TestURLSessionDelegate.swift in Sources */,
|
||||
B594B12521D580BB00F3AC59 /* UninstallCommandSpec.swift in Sources */,
|
||||
B594B13821D6D6C100F3AC59 /* UpgradeCommandSpec.swift in Sources */,
|
||||
B576FDFC21E10A610016B39D /* URLSessionConfiguration+Tests.swift in Sources */,
|
||||
B576FE1D21E28EF70016B39D /* URLSessionDataTaskMock.swift in Sources */,
|
||||
B576FE2E21E5A8010016B39D /* Strongify.swift in Sources */,
|
||||
B5DBF81121DEEC4200F3B151 /* VendorCommandSpec.swift in Sources */,
|
||||
B576FDF521E1078F0016B39D /* MASErrorTestCase.swift in Sources */,
|
||||
B594B13621D6D68600F3AC59 /* VersionCommandSpec.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -998,7 +1200,7 @@
|
|||
);
|
||||
FRAMEWORK_VERSION = A;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = MasKit/Info.plist;
|
||||
INFOPLIST_FILE = MasKit/SupportingFiles/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
|
@ -1036,7 +1238,7 @@
|
|||
);
|
||||
FRAMEWORK_VERSION = A;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = MasKit/Info.plist;
|
||||
INFOPLIST_FILE = MasKit/SupportingFiles/Info.plist;
|
||||
INSTALL_PATH = /Frameworks;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks";
|
||||
MTL_FAST_MATH = YES;
|
||||
|
@ -1068,7 +1270,7 @@
|
|||
"$(PROJECT_DIR)/Carthage/Build/Mac",
|
||||
);
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = MasKitTests/Info.plist;
|
||||
INFOPLIST_FILE = MasKitTests/SupportingFiles/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
|
@ -1098,7 +1300,7 @@
|
|||
"$(PROJECT_DIR)/Carthage/Build/Mac",
|
||||
);
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = MasKitTests/Info.plist;
|
||||
INFOPLIST_FILE = MasKitTests/SupportingFiles/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = at.phatbl.MasKitTests;
|
||||
|
|
Loading…
Reference in a new issue