From f28c2a423209e356c46fbbb76d274567514ef689 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 14 Apr 2021 12:08:55 -0700 Subject: [PATCH 01/24] =?UTF-8?q?=F0=9F=A7=B1=20Import=20only=20Foundation?= =?UTF-8?q?=20instead=20of=20Cocoa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/AppLibrary.swift | 2 ++ MasKit/Controllers/MasStoreSearch.swift | 2 ++ MasKit/Controllers/StoreSearch.swift | 2 ++ MasKit/ExternalCommands/ExternalCommand.swift | 2 ++ MasKit/ExternalCommands/OpenSystemCommand.swift | 2 ++ MasKit/Formatters/Utilities.swift | 2 ++ MasKit/Models/SoftwareProduct.swift | 2 ++ MasKit/Network/NetworkResult.swift | 2 ++ MasKit/Network/NetworkSession.swift | 2 ++ MasKit/SupportingFiles/MasKit.h | 2 +- MasKitTests/ExternalCommands/OpenSystemCommandMock.swift | 1 + MasKitTests/Models/SoftwareProductMock.swift | 1 + MasKitTests/Network/NetworkSessionMock.swift | 1 + MasKitTests/Network/NetworkSessionMockFromFile.swift | 1 + mas-cli.xcodeproj/project.pbxproj | 2 -- 15 files changed, 23 insertions(+), 3 deletions(-) diff --git a/MasKit/Controllers/AppLibrary.swift b/MasKit/Controllers/AppLibrary.swift index ab6d28e46..a881dc2b2 100644 --- a/MasKit/Controllers/AppLibrary.swift +++ b/MasKit/Controllers/AppLibrary.swift @@ -6,6 +6,8 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation + /// Utility for managing installed apps. public protocol AppLibrary { /// Entire set of installed apps. diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index b4f677860..f024e97d0 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -6,6 +6,8 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation + /// Manages searching the MAS catalog through the iTunes Search and Lookup APIs. public class MasStoreSearch: StoreSearch { private let networkManager: NetworkManager diff --git a/MasKit/Controllers/StoreSearch.swift b/MasKit/Controllers/StoreSearch.swift index fcc6a6b21..f7e817b43 100644 --- a/MasKit/Controllers/StoreSearch.swift +++ b/MasKit/Controllers/StoreSearch.swift @@ -6,6 +6,8 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation + /// Protocol for searching the MAS catalog. public protocol StoreSearch { func lookup(app appId: Int) throws -> SearchResult? diff --git a/MasKit/ExternalCommands/ExternalCommand.swift b/MasKit/ExternalCommands/ExternalCommand.swift index 04eec21e5..d8bfa529d 100644 --- a/MasKit/ExternalCommands/ExternalCommand.swift +++ b/MasKit/ExternalCommands/ExternalCommand.swift @@ -6,6 +6,8 @@ // Copyright © 2019 mas-cli. All rights reserved. // +import Foundation + /// CLI command public protocol ExternalCommand { var binaryPath: String { get set } diff --git a/MasKit/ExternalCommands/OpenSystemCommand.swift b/MasKit/ExternalCommands/OpenSystemCommand.swift index a82723f1c..81ed687a7 100644 --- a/MasKit/ExternalCommands/OpenSystemCommand.swift +++ b/MasKit/ExternalCommands/OpenSystemCommand.swift @@ -6,6 +6,8 @@ // Copyright © 2019 mas-cli. All rights reserved. // +import Foundation + /// Wrapper for the external open system command. /// https://ss64.com/osx/open.html public struct OpenSystemCommand: ExternalCommand { diff --git a/MasKit/Formatters/Utilities.swift b/MasKit/Formatters/Utilities.swift index 2b069f286..97ef68a74 100644 --- a/MasKit/Formatters/Utilities.swift +++ b/MasKit/Formatters/Utilities.swift @@ -6,6 +6,8 @@ // Copyright © 2016 Andrew Naylor. All rights reserved. // +import Foundation + /// A collection of output formatting helpers /// Terminal Control Sequence Indicator diff --git a/MasKit/Models/SoftwareProduct.swift b/MasKit/Models/SoftwareProduct.swift index 230697b56..e180040a4 100644 --- a/MasKit/Models/SoftwareProduct.swift +++ b/MasKit/Models/SoftwareProduct.swift @@ -6,6 +6,8 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation + /// Protocol describing the members of CKSoftwareProduct used throughout MasKit. public protocol SoftwareProduct { var appName: String { get } diff --git a/MasKit/Network/NetworkResult.swift b/MasKit/Network/NetworkResult.swift index 8724a4f34..eadc3e02f 100644 --- a/MasKit/Network/NetworkResult.swift +++ b/MasKit/Network/NetworkResult.swift @@ -6,6 +6,8 @@ // Copyright © 2019 mas-cli. All rights reserved. // +import Foundation + enum NetworkResult { case success(Data) case failure(Error) diff --git a/MasKit/Network/NetworkSession.swift b/MasKit/Network/NetworkSession.swift index 64ad4044a..c54f40ffb 100644 --- a/MasKit/Network/NetworkSession.swift +++ b/MasKit/Network/NetworkSession.swift @@ -6,6 +6,8 @@ // Copyright © 2019 mas-cli. All rights reserved. // +import Foundation + @objc public protocol NetworkSession { @objc func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) } diff --git a/MasKit/SupportingFiles/MasKit.h b/MasKit/SupportingFiles/MasKit.h index 3b4b909e4..154841cfc 100644 --- a/MasKit/SupportingFiles/MasKit.h +++ b/MasKit/SupportingFiles/MasKit.h @@ -6,7 +6,7 @@ // Copyright © 2018 Andrew Naylor. All rights reserved. // -@import Cocoa; +@import Foundation; //! Project version number for MasKit. FOUNDATION_EXPORT double MasKitVersionNumber; diff --git a/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift b/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift index 3abedd746..13352e061 100644 --- a/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift +++ b/MasKitTests/ExternalCommands/OpenSystemCommandMock.swift @@ -6,6 +6,7 @@ // Copyright © 2019 mas-cli. All rights reserved. // +import Foundation @testable import MasKit class OpenSystemCommandMock: ExternalCommand { diff --git a/MasKitTests/Models/SoftwareProductMock.swift b/MasKitTests/Models/SoftwareProductMock.swift index 2502d1281..ee8203e51 100644 --- a/MasKitTests/Models/SoftwareProductMock.swift +++ b/MasKitTests/Models/SoftwareProductMock.swift @@ -6,6 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation @testable import MasKit struct SoftwareProductMock: SoftwareProduct { diff --git a/MasKitTests/Network/NetworkSessionMock.swift b/MasKitTests/Network/NetworkSessionMock.swift index 81b15f98b..cd43d5687 100644 --- a/MasKitTests/Network/NetworkSessionMock.swift +++ b/MasKitTests/Network/NetworkSessionMock.swift @@ -6,6 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // +import Foundation import MasKit /// Mock NetworkSession for testing. diff --git a/MasKitTests/Network/NetworkSessionMockFromFile.swift b/MasKitTests/Network/NetworkSessionMockFromFile.swift index 0835c15ef..c2bb89038 100644 --- a/MasKitTests/Network/NetworkSessionMockFromFile.swift +++ b/MasKitTests/Network/NetworkSessionMockFromFile.swift @@ -6,6 +6,7 @@ // Copyright © 2019 mas-cli. All rights reserved. // +import Foundation import MasKit /// Mock NetworkSession for testing with saved JSON response payload files. diff --git a/mas-cli.xcodeproj/project.pbxproj b/mas-cli.xcodeproj/project.pbxproj index 18511b040..c524ba932 100644 --- a/mas-cli.xcodeproj/project.pbxproj +++ b/mas-cli.xcodeproj/project.pbxproj @@ -290,7 +290,6 @@ F80B27B52611116A00A285C9 /* AppListFormatterSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppListFormatterSpec.swift; sourceTree = ""; }; F80B27B92611118E00A285C9 /* AppListFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppListFormatter.swift; sourceTree = ""; }; F8242D8020746A510026DF35 /* StoreAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreAccount.swift; sourceTree = ""; }; - F83213A42173EF75008BA8A0 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; F83213A52173EF75008BA8A0 /* StoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreFoundation.framework; path = /System/Library/PrivateFrameworks/StoreFoundation.framework; sourceTree = ""; }; F83213A62173EF75008BA8A0 /* CommerceKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CommerceKit.framework; path = /System/Library/PrivateFrameworks/CommerceKit.framework; sourceTree = ""; }; F85DA8AD240C313900FE5650 /* MasAppLibrarySpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasAppLibrarySpec.swift; sourceTree = ""; }; @@ -639,7 +638,6 @@ EDFC76381B642A2E00D0DBD7 /* Frameworks */ = { isa = PBXGroup; children = ( - F83213A42173EF75008BA8A0 /* Cocoa.framework */, F83213A62173EF75008BA8A0 /* CommerceKit.framework */, F83213A52173EF75008BA8A0 /* StoreFoundation.framework */, ); From 9916a9f5055627693043f569368690a1366fd027 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 14 Apr 2021 15:03:10 -0700 Subject: [PATCH 02/24] =?UTF-8?q?=F0=9F=8F=83=20Throw=20any=20errors=20fro?= =?UTF-8?q?m=20Process.run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/ExternalCommands/ExternalCommand.swift | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/MasKit/ExternalCommands/ExternalCommand.swift b/MasKit/ExternalCommands/ExternalCommand.swift index d8bfa529d..21f14d68e 100644 --- a/MasKit/ExternalCommands/ExternalCommand.swift +++ b/MasKit/ExternalCommands/ExternalCommand.swift @@ -19,7 +19,7 @@ public protocol ExternalCommand { var stdoutPipe: Pipe { get } var stderrPipe: Pipe { get } - var exitCode: Int? { get } + var exitCode: Int32 { get } var succeeded: Bool { get } var failed: Bool { get } @@ -39,12 +39,12 @@ extension ExternalCommand { return String(data: data, encoding: .utf8) ?? "" } - public var exitCode: Int? { - Int(process.terminationStatus) + public var exitCode: Int32 { + process.terminationStatus } public var succeeded: Bool { - exitCode == 0 + process.terminationReason == .exit && exitCode == 0 } public var failed: Bool { @@ -57,14 +57,9 @@ extension ExternalCommand { process.standardError = stderrPipe process.arguments = arguments - if #available(OSX 10.13, *) { + if #available(macOS 10.13, *) { process.executableURL = URL(fileURLWithPath: binaryPath) - do { - try process.run() - } catch { - printError("Unable to launch command") - // return throw Error() - } + try process.run() } else { process.launchPath = binaryPath process.launch() From cab6314e381ec5402c62416194889ace5a5d8813 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 14 Apr 2021 15:18:57 -0700 Subject: [PATCH 03/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20unused=20ex?= =?UTF-8?q?tensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/AppLibrary.swift | 12 ------------ MasKit/Extensions/Dictionary+StringOrEmpty.swift | 15 --------------- mas-cli.xcodeproj/project.pbxproj | 4 ---- 3 files changed, 31 deletions(-) delete mode 100644 MasKit/Extensions/Dictionary+StringOrEmpty.swift diff --git a/MasKit/Controllers/AppLibrary.swift b/MasKit/Controllers/AppLibrary.swift index a881dc2b2..998812e1f 100644 --- a/MasKit/Controllers/AppLibrary.swift +++ b/MasKit/Controllers/AppLibrary.swift @@ -13,9 +13,6 @@ public protocol AppLibrary { /// Entire set of installed apps. var installedApps: [SoftwareProduct] { get } - /// Map of app name to ID. - var appIdsByName: [String: UInt64] { get } - /// Finds an app by ID. /// /// - Parameter forId: MAS ID for app. @@ -43,15 +40,6 @@ public protocol AppLibrary { /// Common logic extension AppLibrary { - /// Map of app name to ID. - public var appIdsByName: [String: UInt64] { - 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. diff --git a/MasKit/Extensions/Dictionary+StringOrEmpty.swift b/MasKit/Extensions/Dictionary+StringOrEmpty.swift deleted file mode 100644 index b7b658728..000000000 --- a/MasKit/Extensions/Dictionary+StringOrEmpty.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// 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 { - self[key] as? String ?? "" - } -} diff --git a/mas-cli.xcodeproj/project.pbxproj b/mas-cli.xcodeproj/project.pbxproj index c524ba932..1d2be0c0d 100644 --- a/mas-cli.xcodeproj/project.pbxproj +++ b/mas-cli.xcodeproj/project.pbxproj @@ -36,7 +36,6 @@ 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 */; }; @@ -227,7 +226,6 @@ B576FE1521E1D8CB0016B39D /* String+FileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FileExtension.swift"; sourceTree = ""; }; B576FE1A21E28E8A0016B39D /* NetworkSessionMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkSessionMock.swift; sourceTree = ""; }; B576FE1C21E28EF70016B39D /* URLSessionDataTaskMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionDataTaskMock.swift; sourceTree = ""; }; - B576FE2721E423E60016B39D /* Dictionary+StringOrEmpty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+StringOrEmpty.swift"; sourceTree = ""; }; B576FE2921E4240B0016B39D /* AppInfoFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoFormatter.swift; sourceTree = ""; }; B576FE2B21E42A230016B39D /* OutputListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputListener.swift; sourceTree = ""; }; B576FE2D21E5A8010016B39D /* Strongify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Strongify.swift; sourceTree = ""; }; @@ -448,7 +446,6 @@ B576FE1F21E290720016B39D /* Extensions */ = { isa = PBXGroup; children = ( - B576FE2721E423E60016B39D /* Dictionary+StringOrEmpty.swift */, B576FE0D21E1D6310016B39D /* String+PercentEncoding.swift */, ); path = Extensions; @@ -944,7 +941,6 @@ B594B12721D5825800F3AC59 /* AppLibrary.swift in Sources */, F85DA8B2240CBAFE00FE5650 /* CKSoftwareMap+SoftwareMap.swift in Sources */, B594B12B21D5837200F3AC59 /* CKSoftwareProduct+SoftwareProduct.swift in Sources */, - B576FE2821E423E60016B39D /* Dictionary+StringOrEmpty.swift in Sources */, F8FB716A20F2B4DD00F56FDC /* Downloader.swift in Sources */, B588CE0221DC89490047D305 /* ExternalCommand.swift in Sources */, B594B14C21D8983700F3AC59 /* Home.swift in Sources */, From 2891fe985ef46ae87483c1201d1954be8ad8b4f1 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Thu, 8 Apr 2021 16:15:29 -0700 Subject: [PATCH 04/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Remove=20unused=20Pr?= =?UTF-8?q?ivateFrameworks=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommerceKit/CKUpdateController.h | 98 ------------------- .../CommerceKit/ISStoreURLOperationDelegate.h | 22 ----- .../CommerceKit/module.modulemap | 4 - .../StoreFoundation/ISOperationDelegate.h | 19 ---- .../StoreFoundation/ISURLOperationDelegate.h | 21 ---- .../StoreFoundation/module.modulemap | 4 - mas-cli.xcodeproj/project.pbxproj | 16 --- 7 files changed, 184 deletions(-) delete mode 100644 PrivateFrameworks/CommerceKit/CKUpdateController.h delete mode 100644 PrivateFrameworks/CommerceKit/ISStoreURLOperationDelegate.h delete mode 100644 PrivateFrameworks/StoreFoundation/ISOperationDelegate.h delete mode 100644 PrivateFrameworks/StoreFoundation/ISURLOperationDelegate.h diff --git a/PrivateFrameworks/CommerceKit/CKUpdateController.h b/PrivateFrameworks/CommerceKit/CKUpdateController.h deleted file mode 100644 index 6b00b3547..000000000 --- a/PrivateFrameworks/CommerceKit/CKUpdateController.h +++ /dev/null @@ -1,98 +0,0 @@ -// -// Generated by class-dump 3.5 (64 bit). -// -// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. -// - -#import "CKServiceInterface.h" - -@class CKUpdateControllerClient, NSMutableDictionary; - -NS_ASSUME_NONNULL_BEGIN - -@interface CKUpdateController : CKServiceInterface -{ - BOOL _shouldNotAttemptInstallationAfterFailureDialog; - - // CDUnknownBlockType _dialogHandler; - - NSMutableDictionary *_availableUpdatesObservers; - NSMutableDictionary *_updateScanObservers; - NSMutableDictionary *_updateProgressObservers; - CKUpdateControllerClient *_sharedObserver; -} - -+ (CKUpdateController * _Nullable)sharedUpdateController; - -@property(retain, nonatomic) CKUpdateControllerClient *sharedObserver; // @synthesize sharedObserver=_sharedObserver; -@property(retain, nonatomic) NSMutableDictionary *updateProgressObservers; // @synthesize updateProgressObservers=_updateProgressObservers; -@property(retain, nonatomic) NSMutableDictionary *updateScanObservers; // @synthesize updateScanObservers=_updateScanObservers; -@property(retain, nonatomic) NSMutableDictionary *availableUpdatesObservers; // @synthesize availableUpdatesObservers=_availableUpdatesObservers; -@property BOOL shouldNotAttemptInstallationAfterFailureDialog; // @synthesize shouldNotAttemptInstallationAfterFailureDialog=_shouldNotAttemptInstallationAfterFailureDialog; - -//@property(copy) CDUnknownBlockType dialogHandler; // @synthesize dialogHandler=_dialogHandler; -//- (void).cxx_destruct; - -- (void)didInteractivelyPurchaseItemIdentifier:(unsigned long long)arg1 success:(BOOL)arg2; -- (BOOL)willInteractivelyPurchaseItemIdentifier:(unsigned long long)arg1; -- (void)promptUserToOptInForAutoUpdateWithShowNotification:(BOOL)arg1; -- (BOOL)shouldPromptForAutoUpdateOptIn; -- (BOOL)isAutoUpdatedEnabled; -- (id)installedUpdatesJournal; -- (BOOL)softwareUpdateCatalogIsSeedCatalog; -- (long long)softwareUpdateCatalogTrustLevel; -- (int)catalogTrustLevel; -- (id)catalogHostName; -- (void)stopObservingOSUpdateProgressWithCallback:(id)arg1; - -//- (id)observeOSUpdateProgressWithProgressHandler:(CDUnknownBlockType)arg1; - -- (void)stopObservingOSUpdateScansWithCallback:(id)arg1; - -//- (id)observeOSUpdateScansWithProgressHandler:(CDUnknownBlockType)arg1; - -- (void)startOSUpdateScanWithForceFullScan:(BOOL)arg1 reportProgressImmediately:(BOOL)arg2 launchedFromNotification:(BOOL)arg3 userHasSeenAllUpdates:(BOOL)arg4 checkForOtherUpdates:(BOOL)arg5; -- (void)unhideAllOSUpdates; -- (void)hideOSUpdatesWithProductKeys:(id)arg1; -- (BOOL)hasHiddenOSUpdates; -- (BOOL)osUpdateScanInProgress; -- (id)_updateFailureDialogWithAuditInfo:(id)arg1; -- (BOOL)_otherUsersAreLoggedIn; -- (void)showUpdateFailureWithAuditToken:(id)arg1; -- (void)removeUpdateFromInstallLaterWithBundleID:(id)arg1; -- (id)appUpdatesToBeInstalledLater; -- (id)osUpdatesToBeInstalledLater; -- (id)osUpdatesToBeInstalledAfterLogout; -- (void)cancelUpdatesToBeInstalledLater; - -//- (void)queueOSUpdatesForLaterInstall:(id)arg1 withMode:(long long)arg2 completionHandler:(CDUnknownBlockType)arg3; - -- (void)installAvailableUpdatesLaterWithMode:(long long)arg1; -- (BOOL)shouldOfferDoItLater; - -//- (void)updatesWithTags:(id)arg1 completionHandler:(CDUnknownBlockType)arg2; - -- (void)installAllAvailableUpdates; - -//- (void)startAppInstallWithTags:(id)arg1 fallbackPurchase:(id)arg2 completionHandler:(CDUnknownBlockType)arg3; -//- (void)startAppUpdates:(id)arg1 andOSUpdates:(id)arg2 withDelegate:(id)arg3 completionHandler:(CDUnknownBlockType)arg4; -//- (void)_checkForBookUpdatesWithCompletionHandler:(CDUnknownBlockType)arg1; -//- (void)checkForUpdatesWithUserHasSeenUpdates:(BOOL)arg1 completionHandler:(CDUnknownBlockType)arg2; - -- (void)removeAvailableUpdatesObserver:(id)arg1; - -//- (id)addAvailableUpdatesObserverWithBlock:(CDUnknownBlockType)arg1; - -- (unsigned long long)availableUpdatesBadgeCount; -- (id)incompatibleUpdates; - -- (nullable CKUpdate *)availableUpdateWithItemIdentifier:(unsigned long long)arg1; -- (NSArray*)availableUpdates; - -- (void)connectionWasInterrupted; -- (id)initWithStoreClient:(id)arg1; -- (id)init; - -@end - -NS_ASSUME_NONNULL_END diff --git a/PrivateFrameworks/CommerceKit/ISStoreURLOperationDelegate.h b/PrivateFrameworks/CommerceKit/ISStoreURLOperationDelegate.h deleted file mode 100644 index cb15080f7..000000000 --- a/PrivateFrameworks/CommerceKit/ISStoreURLOperationDelegate.h +++ /dev/null @@ -1,22 +0,0 @@ -// -// Generated by class-dump 3.5 (64 bit). -// -// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. -// - -@import StoreFoundation; - -#import "ISURLOperationDelegate.h" - -@class ISStoreURLOperation, NSNumber, NSString; - -NS_ASSUME_NONNULL_BEGIN - -@protocol ISStoreURLOperationDelegate - -@optional -- (BOOL)operation:(ISStoreURLOperation *)arg1 shouldSetStoreFrontID:(NSString *)arg2; -- (void)operation:(ISStoreURLOperation *)arg1 didAuthenticateWithDSID:(NSNumber *)arg2; -@end - -NS_ASSUME_NONNULL_END diff --git a/PrivateFrameworks/CommerceKit/module.modulemap b/PrivateFrameworks/CommerceKit/module.modulemap index ef2067c7e..2ff442f92 100644 --- a/PrivateFrameworks/CommerceKit/module.modulemap +++ b/PrivateFrameworks/CommerceKit/module.modulemap @@ -6,13 +6,9 @@ module CommerceKit { header "CKAccountStore.h" header "CKDownloadDirectory.h" - header "CKDownloadQueue.h" header "CKDownloadQueueObserver.h" header "CKPurchaseController.h" - header "CKServiceInterface.h" header "CKSoftwareMap.h" - header "CKUpdateController.h" - header "ISStoreURLOperationDelegate.h" export * } diff --git a/PrivateFrameworks/StoreFoundation/ISOperationDelegate.h b/PrivateFrameworks/StoreFoundation/ISOperationDelegate.h deleted file mode 100644 index a686addb7..000000000 --- a/PrivateFrameworks/StoreFoundation/ISOperationDelegate.h +++ /dev/null @@ -1,19 +0,0 @@ -// -// Generated by class-dump 3.5 (64 bit). -// -// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. -// - -@class ISOperation, NSError, SSOperationProgress; - -NS_ASSUME_NONNULL_BEGIN - -@protocol ISOperationDelegate - -@optional -- (void)operationFinished:(ISOperation *)arg1; -- (void)operation:(ISOperation *)arg1 updatedProgress:(SSOperationProgress *)arg2; -- (void)operation:(ISOperation *)arg1 failedWithError:(NSError *)arg2; -@end - -NS_ASSUME_NONNULL_END diff --git a/PrivateFrameworks/StoreFoundation/ISURLOperationDelegate.h b/PrivateFrameworks/StoreFoundation/ISURLOperationDelegate.h deleted file mode 100644 index 9a8d7b92b..000000000 --- a/PrivateFrameworks/StoreFoundation/ISURLOperationDelegate.h +++ /dev/null @@ -1,21 +0,0 @@ -// -// Generated by class-dump 3.5 (64 bit). -// -// class-dump is Copyright (C) 1997-1998, 2000-2001, 2004-2013 by Steve Nygard. -// - - #import "ISOperationDelegate.h" - -@class ISURLOperation, NSMutableURLRequest, NSURLResponse; - -NS_ASSUME_NONNULL_BEGIN - -@protocol ISURLOperationDelegate - -@optional -- (void)operation:(ISURLOperation *)arg1 willSendRequest:(NSMutableURLRequest *)arg2; -- (void)operation:(ISURLOperation *)arg1 didReceiveResponse:(NSURLResponse *)arg2; -- (void)operation:(ISURLOperation *)arg1 finishedWithOutput:(id)arg2; -@end - -NS_ASSUME_NONNULL_END diff --git a/PrivateFrameworks/StoreFoundation/module.modulemap b/PrivateFrameworks/StoreFoundation/module.modulemap index 197ea3ea3..96344c688 100644 --- a/PrivateFrameworks/StoreFoundation/module.modulemap +++ b/PrivateFrameworks/StoreFoundation/module.modulemap @@ -8,12 +8,8 @@ module StoreFoundation { header "CKUpdate.h" header "ISAccountService.h" header "ISAuthenticationContext.h" - header "ISOperationDelegate.h" - header "ISServiceProxy.h" - header "ISServiceRemoteObject.h" header "ISStoreAccount.h" header "ISStoreClient.h" - header "ISURLOperationDelegate.h" header "SSDownload.h" header "SSDownloadMetadata.h" header "SSDownloadPhase.h" diff --git a/mas-cli.xcodeproj/project.pbxproj b/mas-cli.xcodeproj/project.pbxproj index 1d2be0c0d..ac84e4165 100644 --- a/mas-cli.xcodeproj/project.pbxproj +++ b/mas-cli.xcodeproj/project.pbxproj @@ -85,19 +85,15 @@ F832138C2173D3E1008BA8A0 /* CKPurchaseController.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB719D20F2EC4500F56FDC /* CKPurchaseController.h */; }; F832138D2173D3E1008BA8A0 /* CKServiceInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB719E20F2EC4500F56FDC /* CKServiceInterface.h */; }; F832138E2173D3E1008BA8A0 /* CKSoftwareMap.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB719F20F2EC4500F56FDC /* CKSoftwareMap.h */; }; - F832138F2173D3E1008BA8A0 /* CKUpdateController.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71A020F2EC4500F56FDC /* CKUpdateController.h */; }; - F83213902173D3E1008BA8A0 /* ISStoreURLOperationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71A120F2EC4500F56FDC /* ISStoreURLOperationDelegate.h */; }; F83213912173D3E1008BA8A0 /* CKDownloadDirectory.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71B620F2F87300F56FDC /* CKDownloadDirectory.h */; }; F83213922173D5AB008BA8A0 /* CKSoftwareProduct.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71A320F2EC4500F56FDC /* CKSoftwareProduct.h */; }; F83213932173D5AB008BA8A0 /* CKUpdate.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71A420F2EC4500F56FDC /* CKUpdate.h */; }; F83213942173D5AB008BA8A0 /* ISAccountService.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71A520F2EC4500F56FDC /* ISAccountService.h */; }; F83213952173D5AB008BA8A0 /* ISAuthenticationContext.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71A620F2EC4500F56FDC /* ISAuthenticationContext.h */; }; - F83213962173D5AB008BA8A0 /* ISOperationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71A720F2EC4500F56FDC /* ISOperationDelegate.h */; }; F83213972173D5AB008BA8A0 /* ISServiceProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71A820F2EC4500F56FDC /* ISServiceProxy.h */; }; F83213982173D5AB008BA8A0 /* ISServiceRemoteObject.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71A920F2EC4500F56FDC /* ISServiceRemoteObject.h */; }; F83213992173D5AB008BA8A0 /* ISStoreAccount.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71AA20F2EC4500F56FDC /* ISStoreAccount.h */; }; F832139A2173D5AB008BA8A0 /* ISStoreClient.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71AB20F2EC4500F56FDC /* ISStoreClient.h */; }; - F832139B2173D5AB008BA8A0 /* ISURLOperationDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71AC20F2EC4500F56FDC /* ISURLOperationDelegate.h */; }; F832139C2173D5B2008BA8A0 /* SSDownload.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71AD20F2EC4500F56FDC /* SSDownload.h */; }; F832139D2173D5B2008BA8A0 /* SSDownloadMetadata.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71AE20F2EC4500F56FDC /* SSDownloadMetadata.h */; }; F832139E2173D5B2008BA8A0 /* SSDownloadPhase.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB71AF20F2EC4500F56FDC /* SSDownloadPhase.h */; }; @@ -306,18 +302,14 @@ F8FB719D20F2EC4500F56FDC /* CKPurchaseController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CKPurchaseController.h; sourceTree = ""; }; F8FB719E20F2EC4500F56FDC /* CKServiceInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CKServiceInterface.h; sourceTree = ""; }; F8FB719F20F2EC4500F56FDC /* CKSoftwareMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CKSoftwareMap.h; sourceTree = ""; }; - F8FB71A020F2EC4500F56FDC /* CKUpdateController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CKUpdateController.h; sourceTree = ""; }; - F8FB71A120F2EC4500F56FDC /* ISStoreURLOperationDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ISStoreURLOperationDelegate.h; sourceTree = ""; }; F8FB71A320F2EC4500F56FDC /* CKSoftwareProduct.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CKSoftwareProduct.h; sourceTree = ""; }; F8FB71A420F2EC4500F56FDC /* CKUpdate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CKUpdate.h; sourceTree = ""; }; F8FB71A520F2EC4500F56FDC /* ISAccountService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ISAccountService.h; sourceTree = ""; }; F8FB71A620F2EC4500F56FDC /* ISAuthenticationContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ISAuthenticationContext.h; sourceTree = ""; }; - F8FB71A720F2EC4500F56FDC /* ISOperationDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ISOperationDelegate.h; sourceTree = ""; }; F8FB71A820F2EC4500F56FDC /* ISServiceProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ISServiceProxy.h; sourceTree = ""; }; F8FB71A920F2EC4500F56FDC /* ISServiceRemoteObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ISServiceRemoteObject.h; sourceTree = ""; }; F8FB71AA20F2EC4500F56FDC /* ISStoreAccount.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ISStoreAccount.h; sourceTree = ""; }; F8FB71AB20F2EC4500F56FDC /* ISStoreClient.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ISStoreClient.h; sourceTree = ""; }; - F8FB71AC20F2EC4500F56FDC /* ISURLOperationDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ISURLOperationDelegate.h; sourceTree = ""; }; F8FB71AD20F2EC4500F56FDC /* SSDownload.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SSDownload.h; sourceTree = ""; }; F8FB71AE20F2EC4500F56FDC /* SSDownloadMetadata.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SSDownloadMetadata.h; sourceTree = ""; }; F8FB71AF20F2EC4500F56FDC /* SSDownloadPhase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SSDownloadPhase.h; sourceTree = ""; }; @@ -698,8 +690,6 @@ F8FB719D20F2EC4500F56FDC /* CKPurchaseController.h */, F8FB719E20F2EC4500F56FDC /* CKServiceInterface.h */, F8FB719F20F2EC4500F56FDC /* CKSoftwareMap.h */, - F8FB71A020F2EC4500F56FDC /* CKUpdateController.h */, - F8FB71A120F2EC4500F56FDC /* ISStoreURLOperationDelegate.h */, F8FB71B320F2EC7900F56FDC /* module.modulemap */, ); path = CommerceKit; @@ -713,12 +703,10 @@ F8FB71A520F2EC4500F56FDC /* ISAccountService.h */, F8FB71A620F2EC4500F56FDC /* ISAuthenticationContext.h */, B578F060224FB5BD00D2086A /* ISAuthenticationResponse.h */, - F8FB71A720F2EC4500F56FDC /* ISOperationDelegate.h */, F8FB71A820F2EC4500F56FDC /* ISServiceProxy.h */, F8FB71A920F2EC4500F56FDC /* ISServiceRemoteObject.h */, F8FB71AA20F2EC4500F56FDC /* ISStoreAccount.h */, F8FB71AB20F2EC4500F56FDC /* ISStoreClient.h */, - F8FB71AC20F2EC4500F56FDC /* ISURLOperationDelegate.h */, F8FB71B420F2EC8800F56FDC /* module.modulemap */, F8FB71AD20F2EC4500F56FDC /* SSDownload.h */, F8FB71AE20F2EC4500F56FDC /* SSDownloadMetadata.h */, @@ -746,17 +734,13 @@ F832138E2173D3E1008BA8A0 /* CKSoftwareMap.h in Headers */, F83213922173D5AB008BA8A0 /* CKSoftwareProduct.h in Headers */, F83213932173D5AB008BA8A0 /* CKUpdate.h in Headers */, - F832138F2173D3E1008BA8A0 /* CKUpdateController.h in Headers */, F83213942173D5AB008BA8A0 /* ISAccountService.h in Headers */, F83213952173D5AB008BA8A0 /* ISAuthenticationContext.h in Headers */, B578F061224FB5BD00D2086A /* ISAuthenticationResponse.h in Headers */, - F83213962173D5AB008BA8A0 /* ISOperationDelegate.h in Headers */, F83213972173D5AB008BA8A0 /* ISServiceProxy.h in Headers */, F83213982173D5AB008BA8A0 /* ISServiceRemoteObject.h in Headers */, F83213992173D5AB008BA8A0 /* ISStoreAccount.h in Headers */, F832139A2173D5AB008BA8A0 /* ISStoreClient.h in Headers */, - F83213902173D3E1008BA8A0 /* ISStoreURLOperationDelegate.h in Headers */, - F832139B2173D5AB008BA8A0 /* ISURLOperationDelegate.h in Headers */, F8FB716220F2B41400F56FDC /* MasKit.h in Headers */, F832139C2173D5B2008BA8A0 /* SSDownload.h in Headers */, F832139D2173D5B2008BA8A0 /* SSDownloadMetadata.h in Headers */, From e6dd54b227c8d6d9fedcd8bf336a0ebf622d84be Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Fri, 16 Apr 2021 16:47:13 -0700 Subject: [PATCH 05/24] =?UTF-8?q?=F0=9F=94=8D=20Upgrade=20only=20to=20newe?= =?UTF-8?q?r=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Commands/Outdated.swift | 2 +- MasKit/Commands/Upgrade.swift | 2 +- MasKit/Models/SoftwareProduct.swift | 16 +++++++++++ mas-cli.xcodeproj/project.pbxproj | 27 +++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 16 +++++++++++ 5 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 mas.xcworkspace/xcshareddata/swiftpm/Package.resolved diff --git a/MasKit/Commands/Outdated.swift b/MasKit/Commands/Outdated.swift index 320db1536..7179a7b98 100644 --- a/MasKit/Commands/Outdated.swift +++ b/MasKit/Commands/Outdated.swift @@ -36,7 +36,7 @@ public struct OutdatedCommand: CommandProtocol { for installedApp in appLibrary.installedApps { do { if let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue) { - if installedApp.bundleVersion != storeApp.version { + if installedApp.isOutdatedWhenComparedTo(storeApp) { print( """ \(installedApp.itemIdentifier) \(installedApp.appName) \ diff --git a/MasKit/Commands/Upgrade.swift b/MasKit/Commands/Upgrade.swift index b1d5770ea..5a4b15fd2 100644 --- a/MasKit/Commands/Upgrade.swift +++ b/MasKit/Commands/Upgrade.swift @@ -49,7 +49,7 @@ public struct UpgradeCommand: CommandProtocol { .compactMap { (installedApp: SoftwareProduct) -> SoftwareProduct? in // only upgrade apps whose local version differs from the store version if let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue) { - return storeApp.version != installedApp.bundleVersion + return installedApp.isOutdatedWhenComparedTo(storeApp) ? installedApp : nil } else { diff --git a/MasKit/Models/SoftwareProduct.swift b/MasKit/Models/SoftwareProduct.swift index e180040a4..aa1de2877 100644 --- a/MasKit/Models/SoftwareProduct.swift +++ b/MasKit/Models/SoftwareProduct.swift @@ -7,6 +7,7 @@ // import Foundation +import Version /// Protocol describing the members of CKSoftwareProduct used throughout MasKit. public protocol SoftwareProduct { @@ -31,4 +32,19 @@ extension SoftwareProduct { var appNameOrBbundleIdentifier: String { appName == "" ? bundleIdentifier : appName } + + func isOutdatedWhenComparedTo(_ storeApp: SearchResult) -> Bool { + // The App Store does not enforce semantic versioning, but we assume most apps follow versioning + // schemes that increase numerically over time. + guard let semanticBundleVersion = Version(tolerant: bundleVersion), + let semanticAppStoreVersion = Version(tolerant: storeApp.version) + else { + // If a version string can't be parsed as a Semantic Version, our best effort is to check for + // equality. The only version that matters is the one in the App Store. + // https://semver.org + return bundleVersion != storeApp.version + } + + return semanticBundleVersion < semanticAppStoreVersion + } } diff --git a/mas-cli.xcodeproj/project.pbxproj b/mas-cli.xcodeproj/project.pbxproj index ac84e4165..3e96bfaf5 100644 --- a/mas-cli.xcodeproj/project.pbxproj +++ b/mas-cli.xcodeproj/project.pbxproj @@ -76,6 +76,7 @@ B5DBF81321DEEC7C00F3B151 /* OpenCommandSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF81221DEEC7C00F3B151 /* OpenCommandSpec.swift */; }; B5DBF81521E02BA900F3B151 /* StoreSearchMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF81421E02BA900F3B151 /* StoreSearchMock.swift */; }; B5DBF81721E02E3400F3B151 /* OpenSystemCommandMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5DBF81621E02E3400F3B151 /* OpenSystemCommandMock.swift */; }; + C56C4FF5262A50F5004F37EB /* Version in Frameworks */ = {isa = PBXBuildFile; productRef = C56C4FF4262A50F5004F37EB /* Version */; }; ED031A7C1B5127C00097692E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED031A7B1B5127C00097692E /* main.swift */; }; F80B27B62611116A00A285C9 /* AppListFormatterSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80B27B52611116A00A285C9 /* AppListFormatterSpec.swift */; }; F80B27BA2611118E00A285C9 /* AppListFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80B27B92611118E00A285C9 /* AppListFormatter.swift */; }; @@ -336,6 +337,7 @@ buildActionMask = 2147483647; files = ( F83213A22173DC13008BA8A0 /* Commandant.framework in Frameworks */, + C56C4FF5262A50F5004F37EB /* Version in Frameworks */, B5552928219A1BB900ACB4CA /* CommerceKit.framework in Frameworks */, B5552929219A1BC700ACB4CA /* StoreFoundation.framework in Frameworks */, ); @@ -790,6 +792,9 @@ dependencies = ( ); name = MasKit; + packageProductDependencies = ( + C56C4FF4262A50F5004F37EB /* Version */, + ); productName = MasKit; productReference = F8FB715220F2B41400F56FDC /* MasKit.framework */; productType = "com.apple.product-type.framework"; @@ -850,6 +855,9 @@ Base, ); mainGroup = ED031A6F1B5127C00097692E; + packageReferences = ( + C56C4FF0262A4ED0004F37EB /* XCRemoteSwiftPackageReference "Version" */, + ); productRefGroup = ED031A791B5127C00097692E /* Products */; projectDirPath = ""; projectRoot = ""; @@ -1398,6 +1406,25 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + C56C4FF0262A4ED0004F37EB /* XCRemoteSwiftPackageReference "Version" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/mxcl/Version.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + C56C4FF4262A50F5004F37EB /* Version */ = { + isa = XCSwiftPackageProductDependency; + package = C56C4FF0262A4ED0004F37EB /* XCRemoteSwiftPackageReference "Version" */; + productName = Version; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = ED031A701B5127C00097692E /* Project object */; } diff --git a/mas.xcworkspace/xcshareddata/swiftpm/Package.resolved b/mas.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 000000000..5c4f7c3d6 --- /dev/null +++ b/mas.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "Version", + "repositoryURL": "https://github.com/mxcl/Version.git", + "state": { + "branch": null, + "revision": "a94b48f36763c05629fc102837398505032dead9", + "version": "2.0.0" + } + } + ] + }, + "version": 1 +} From 60cc16ab41ec1b147d1cd814beb774a952abae05 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Fri, 16 Apr 2021 19:22:55 -0700 Subject: [PATCH 06/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20loadDataS?= =?UTF-8?q?ync=20to=20reduce=20code=20duplication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 24 ++----------------- MasKit/Network/NetworkManager.swift | 17 +++++++++---- MasKitTests/Network/NetworkManagerTests.swift | 16 +++++++++---- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index f024e97d0..70fe999d3 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -26,17 +26,7 @@ public class MasStoreSearch: StoreSearch { 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 - } - + let data = try networkManager.loadDataSync(from: url) do { let results = try JSONDecoder().decode(SearchResultList.self, from: data) return results @@ -54,17 +44,7 @@ public class MasStoreSearch: StoreSearch { 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 - } - + let data = try networkManager.loadDataSync(from: url) do { let results = try JSONDecoder().decode(SearchResultList.self, from: data) diff --git a/MasKit/Network/NetworkManager.swift b/MasKit/Network/NetworkManager.swift index a76612fdb..e406f3453 100644 --- a/MasKit/Network/NetworkManager.swift +++ b/MasKit/Network/NetworkManager.swift @@ -47,8 +47,8 @@ public class NetworkManager { /// 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 { + /// - Returns: The Data of the response. + func loadDataSync(from url: URL) throws -> Data { var syncResult: NetworkResult? let semaphore = DispatchSemaphore(value: 0) @@ -59,9 +59,18 @@ public class NetworkManager { _ = semaphore.wait(timeout: .distantFuture) guard let result = syncResult else { - return .failure(NetworkError.timeout) + throw NetworkError.timeout } - return result + // Unwrap network result + guard case let .success(data) = result + else { + if case let .failure(error) = result { + throw error + } + throw MASError.noData + } + + return data } } diff --git a/MasKitTests/Network/NetworkManagerTests.swift b/MasKitTests/Network/NetworkManagerTests.swift index 9f5c4fcb8..259135773 100644 --- a/MasKitTests/Network/NetworkManagerTests.swift +++ b/MasKitTests/Network/NetworkManagerTests.swift @@ -29,7 +29,7 @@ class NetworkManagerTests: XCTestCase { XCTAssertEqual(result, NetworkResult.success(data)) } - func testSuccessfulSyncResponse() { + func testSuccessfulSyncResponse() throws { // Setup our objects let session = NetworkSessionMock() let manager = NetworkManager(session: session) @@ -42,8 +42,8 @@ class NetworkManagerTests: XCTestCase { let url = URL(fileURLWithPath: "url") // Perform the request and verify the result - let result = manager.loadDataSync(from: url) - XCTAssertEqual(result, NetworkResult.success(data)) + let result = try manager.loadDataSync(from: url) + XCTAssertEqual(result, data) } func testFailureAsyncResponse() { @@ -73,7 +73,13 @@ class NetworkManagerTests: XCTestCase { 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)) + XCTAssertThrowsError(try manager.loadDataSync(from: url)) { error in + guard let error = error as? NetworkManager.NetworkError else { + XCTFail("Error is of unexpected type.") + return + } + + XCTAssertEqual(error, NetworkManager.NetworkError.timeout) + } } } From 877cb62872de2c9cd49d0aabe1c8c3effdc4bd65 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Fri, 16 Apr 2021 19:29:05 -0700 Subject: [PATCH 07/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20MasStoreS?= =?UTF-8?q?earch=20to=20reduce=20code=20duplication?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 70fe999d3..84dd94b2a 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -26,13 +26,7 @@ public class MasStoreSearch: StoreSearch { guard let url = searchURL(for: appName) else { throw MASError.urlEncoding } - let data = try networkManager.loadDataSync(from: url) - do { - let results = try JSONDecoder().decode(SearchResultList.self, from: data) - return results - } catch { - throw MASError.jsonParsing(error: error as NSError) - } + return try loadSearchResults(url) } /// Looks up app details. @@ -44,14 +38,17 @@ public class MasStoreSearch: StoreSearch { guard let url = lookupURL(forApp: appId) else { throw MASError.urlEncoding } - let data = try networkManager.loadDataSync(from: url) - do { - let results = try JSONDecoder().decode(SearchResultList.self, from: data) + let results = try loadSearchResults(url) + guard let searchResult = results.results.first + else { return nil } - guard let searchResult = results.results.first - else { return nil } + return searchResult + } - return searchResult + private func loadSearchResults(_ url: URL) throws -> SearchResultList { + let data = try networkManager.loadDataSync(from: url) + do { + return try JSONDecoder().decode(SearchResultList.self, from: data) } catch { throw MASError.jsonParsing(error: error as NSError) } From 763de0f1b5a581b1bd3b1a70f870f6bfa5f9e6ef Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Fri, 16 Apr 2021 20:37:34 -0700 Subject: [PATCH 08/24] =?UTF-8?q?=F0=9F=94=8D=20Examine=20app=20page=20for?= =?UTF-8?q?=20version=20newer=20than=20in=20search=20result?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 38 ++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 84dd94b2a..b0ca0590f 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -7,6 +7,7 @@ // import Foundation +import Version /// Manages searching the MAS catalog through the iTunes Search and Lookup APIs. public class MasStoreSearch: StoreSearch { @@ -46,11 +47,46 @@ public class MasStoreSearch: StoreSearch { } private func loadSearchResults(_ url: URL) throws -> SearchResultList { + var results: SearchResultList let data = try networkManager.loadDataSync(from: url) do { - return try JSONDecoder().decode(SearchResultList.self, from: data) + results = try JSONDecoder().decode(SearchResultList.self, from: data) } catch { throw MASError.jsonParsing(error: error as NSError) } + + // The App Store often lists a newer version available in an app's page than in + // the search results. We attempt to scrape it here. + for index in results.results.indices { + let result = results.results[index] + guard let searchVersion = Version(tolerant: result.version), + let pageUrl = URL(string: result.trackViewUrl) + else { + continue + } + + let pageData: Data + do { + pageData = try networkManager.loadDataSync(from: pageUrl) + } catch { + continue + } + + let html = String(decoding: pageData, as: UTF8.self) + let regex = try NSRegularExpression(pattern: #"\"versionDisplay\"\:\"([^\"]+)\""#) + let fullRange = NSRange(html.startIndex.. searchVersion { + results.results[index].version = pageVersion.description + } + } + + return results } } From 4c99643a1c3fb26c07171d65c15ab2422892165f Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Fri, 16 Apr 2021 21:56:46 -0700 Subject: [PATCH 09/24] =?UTF-8?q?=F0=9F=8F=83=20Load=20app=20pages=20async?= =?UTF-8?q?hronously=20when=20scraping?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 47 ++++++++++++++++--------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index b0ca0590f..61890d298 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -55,8 +55,16 @@ public class MasStoreSearch: StoreSearch { throw MASError.jsonParsing(error: error as NSError) } + let regex: NSRegularExpression + do { + regex = try NSRegularExpression(pattern: #"\"versionDisplay\"\:\"([^\"]+)\""#) + } catch { + return results + } + // The App Store often lists a newer version available in an app's page than in // the search results. We attempt to scrape it here. + let group = DispatchGroup() for index in results.results.indices { let result = results.results[index] guard let searchVersion = Version(tolerant: result.version), @@ -65,28 +73,33 @@ public class MasStoreSearch: StoreSearch { continue } - let pageData: Data - do { - pageData = try networkManager.loadDataSync(from: pageUrl) - } catch { - continue - } + group.enter() + networkManager.loadData(from: pageUrl) { result in + guard case let .success(pageData) = result else { + group.leave() + return + } - let html = String(decoding: pageData, as: UTF8.self) - let regex = try NSRegularExpression(pattern: #"\"versionDisplay\"\:\"([^\"]+)\""#) - let fullRange = NSRange(html.startIndex.. searchVersion { - results.results[index].version = pageVersion.description + if pageVersion > searchVersion { + results.results[index].version = pageVersion.description + } + + group.leave() } } + group.wait() + return results } } From f505664d9d79d9103640198dfaa0907475b7cac8 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 15:00:34 -0700 Subject: [PATCH 10/24] =?UTF-8?q?=E2=8F=B3=20Simplify=20waits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/AppStore/Downloader.swift | 2 +- MasKit/Network/NetworkManager.swift | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/MasKit/AppStore/Downloader.swift b/MasKit/AppStore/Downloader.swift index 1d8ae020c..75b0ef83d 100644 --- a/MasKit/AppStore/Downloader.swift +++ b/MasKit/AppStore/Downloader.swift @@ -57,7 +57,7 @@ func download(_ adamId: UInt64, purchase: Bool = false) -> MASError? { } } - _ = group.wait(timeout: .distantFuture) + group.wait() if let observerIdentifier = observerIdentifier { CKDownloadQueue.shared().remove(observerIdentifier) diff --git a/MasKit/Network/NetworkManager.swift b/MasKit/Network/NetworkManager.swift index e406f3453..51a8a299f 100644 --- a/MasKit/Network/NetworkManager.swift +++ b/MasKit/Network/NetworkManager.swift @@ -50,13 +50,14 @@ public class NetworkManager { /// - Returns: The Data of the response. func loadDataSync(from url: URL) throws -> Data { var syncResult: NetworkResult? - let semaphore = DispatchSemaphore(value: 0) - + let group = DispatchGroup() + group.enter() loadData(from: url) { asyncResult in syncResult = asyncResult - semaphore.signal() + group.leave() } - _ = semaphore.wait(timeout: .distantFuture) + + group.wait() guard let result = syncResult else { throw NetworkError.timeout From ecba9e8608320ba5b66bd6d210a52413bb57c138 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 15:13:38 -0700 Subject: [PATCH 11/24] =?UTF-8?q?=F0=9F=A7=B9=20Lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Commands/Install.swift | 2 +- MasKit/Commands/Lucky.swift | 2 +- MasKit/Commands/Purchase.swift | 2 +- MasKitTests/Controllers/AppLibraryMock.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/MasKit/Commands/Install.swift b/MasKit/Commands/Install.swift index 92e696824..cbfcfaa99 100644 --- a/MasKit/Commands/Install.swift +++ b/MasKit/Commands/Install.swift @@ -31,7 +31,7 @@ public struct InstallCommand: CommandProtocol { /// Runs the command. public func run(_ options: Options) -> Result { // Try to download applications with given identifiers and collect results - let downloadResults = options.appIds.compactMap { (appId) -> MASError? in + let downloadResults = options.appIds.compactMap { appId -> MASError? in if let product = appLibrary.installedApp(forId: appId), !options.forceInstall { printWarning("\(product.appName) is already installed") return nil diff --git a/MasKit/Commands/Lucky.swift b/MasKit/Commands/Lucky.swift index 520ac86df..4c9f4f063 100644 --- a/MasKit/Commands/Lucky.swift +++ b/MasKit/Commands/Lucky.swift @@ -70,7 +70,7 @@ public struct LuckyCommand: CommandProtocol { fileprivate func install(_ appId: UInt64, options: Options) -> Result { // Try to download applications with given identifiers and collect results let downloadResults = [appId] - .compactMap { (appId) -> MASError? in + .compactMap { appId -> MASError? in if let product = appLibrary.installedApp(forId: appId), !options.forceInstall { printWarning("\(product.appName) is already installed") return nil diff --git a/MasKit/Commands/Purchase.swift b/MasKit/Commands/Purchase.swift index 40b80ae8d..f9ea5dc03 100644 --- a/MasKit/Commands/Purchase.swift +++ b/MasKit/Commands/Purchase.swift @@ -30,7 +30,7 @@ public struct PurchaseCommand: CommandProtocol { /// Runs the command. public func run(_ options: Options) -> Result { // Try to download applications with given identifiers and collect results - let downloadResults = options.appIds.compactMap { (appId) -> MASError? in + let downloadResults = options.appIds.compactMap { appId -> MASError? in if let product = appLibrary.installedApp(forId: appId) { printWarning("\(product.appName) has already been purchased.") return nil diff --git a/MasKitTests/Controllers/AppLibraryMock.swift b/MasKitTests/Controllers/AppLibraryMock.swift index 61d70c470..1c250b49f 100644 --- a/MasKitTests/Controllers/AppLibraryMock.swift +++ b/MasKitTests/Controllers/AppLibraryMock.swift @@ -20,7 +20,7 @@ class AppLibraryMock: AppLibrary { } func uninstallApp(app: SoftwareProduct) throws { - if !installedApps.contains(where: { (product) -> Bool in + if !installedApps.contains(where: { product -> Bool in app.itemIdentifier == product.itemIdentifier }) { throw MASError.notInstalled From 03ec323652ba9166ce4edb33735ea5eada595f24 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 15:14:36 -0700 Subject: [PATCH 12/24] =?UTF-8?q?=F0=9F=8F=83=20Initialize=20NSRegularExpr?= =?UTF-8?q?ession=20once?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 61890d298..88ac7f94a 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -12,6 +12,13 @@ import Version /// Manages searching the MAS catalog through the iTunes Search and Lookup APIs. public class MasStoreSearch: StoreSearch { private let networkManager: NetworkManager + private static let versionExpression: NSRegularExpression? = { + do { + return try NSRegularExpression(pattern: #"\"versionDisplay\"\:\"([^\"]+)\""#) + } catch { + return nil + } + }() /// Designated initializer. public init(networkManager: NetworkManager = NetworkManager()) { @@ -55,13 +62,6 @@ public class MasStoreSearch: StoreSearch { throw MASError.jsonParsing(error: error as NSError) } - let regex: NSRegularExpression - do { - regex = try NSRegularExpression(pattern: #"\"versionDisplay\"\:\"([^\"]+)\""#) - } catch { - return results - } - // The App Store often lists a newer version available in an app's page than in // the search results. We attempt to scrape it here. let group = DispatchGroup() @@ -82,7 +82,7 @@ public class MasStoreSearch: StoreSearch { let html = String(decoding: pageData, as: UTF8.self) let fullRange = NSRange(html.startIndex.. Date: Wed, 21 Apr 2021 15:29:03 -0700 Subject: [PATCH 13/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20scrapeVers?= =?UTF-8?q?ionFromPage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 44 ++++++++++++++----------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 88ac7f94a..582e1c605 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -62,8 +62,6 @@ public class MasStoreSearch: StoreSearch { throw MASError.jsonParsing(error: error as NSError) } - // The App Store often lists a newer version available in an app's page than in - // the search results. We attempt to scrape it here. let group = DispatchGroup() for index in results.results.indices { let result = results.results[index] @@ -74,23 +72,8 @@ public class MasStoreSearch: StoreSearch { } group.enter() - networkManager.loadData(from: pageUrl) { result in - guard case let .success(pageData) = result else { - group.leave() - return - } - - let html = String(decoding: pageData, as: UTF8.self) - let fullRange = NSRange(html.startIndex.. searchVersion { + scrapeVersionFromPage(pageUrl) { pageVersion in + if let pageVersion = pageVersion, pageVersion > searchVersion { results.results[index].version = pageVersion.description } @@ -102,4 +85,27 @@ public class MasStoreSearch: StoreSearch { return results } + + // The App Store often lists a newer version available in an app's page than in + // the search results. We attempt to scrape it here. + private func scrapeVersionFromPage(_ pageUrl: URL, _ completion: @escaping (Version?) -> Void) { + networkManager.loadData(from: pageUrl) { result in + guard case let .success(pageData) = result else { + completion(nil) + return + } + + let html = String(decoding: pageData, as: UTF8.self) + let fullRange = NSRange(html.startIndex.. Date: Wed, 21 Apr 2021 15:52:53 -0700 Subject: [PATCH 14/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20StoreSear?= =?UTF-8?q?ch=20into=20asynchronous=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 102 +++++++++++------- MasKit/Controllers/StoreSearch.swift | 56 +++++++++- MasKitTests/Controllers/StoreSearchMock.swift | 19 ++-- MasKitTests/Controllers/StoreSearchSpec.swift | 9 +- 4 files changed, 138 insertions(+), 48 deletions(-) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 582e1c605..26ab16a30 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -28,62 +28,90 @@ public class MasStoreSearch: StoreSearch { /// 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 { + /// - Parameter completion: A closure that receives the search results or an Error if there is a + /// problem with the network request. List will have no records if there were no matches. + public func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) { guard let url = searchURL(for: appName) - else { throw MASError.urlEncoding } + else { + completion(nil, MASError.urlEncoding) + return + } + + loadSearchResults(url) { results, error in + if let error = error { + completion(nil, error) + return + } - return try loadSearchResults(url) + completion(results, nil) + } } /// 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: Int) throws -> SearchResult? { + /// - Parameter completion: A closure that receives the search result record of app, or nil if no apps match the ID, + /// or an Error if there is a problem with the network request. + public func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) { guard let url = lookupURL(forApp: appId) - else { throw MASError.urlEncoding } + else { + completion(nil, MASError.urlEncoding) + return + } - let results = try loadSearchResults(url) - guard let searchResult = results.results.first - else { return nil } + loadSearchResults(url) { results, error in + if let error = error { + completion(nil, error) + return + } - return searchResult + completion(results?.results.first, nil) + } } - private func loadSearchResults(_ url: URL) throws -> SearchResultList { - var results: SearchResultList - let data = try networkManager.loadDataSync(from: url) - do { - results = try JSONDecoder().decode(SearchResultList.self, from: data) - } catch { - throw MASError.jsonParsing(error: error as NSError) - } + public func loadSearchResults(_ url: URL, _ completion: @escaping (SearchResultList?, Error?) -> Void) { + networkManager.loadData(from: url) { result in + guard case let .success(data) = result else { + if case let .failure(error) = result { + completion(nil, error) + } else { + completion(nil, MASError.noData) + } - let group = DispatchGroup() - for index in results.results.indices { - let result = results.results[index] - guard let searchVersion = Version(tolerant: result.version), - let pageUrl = URL(string: result.trackViewUrl) - else { - continue + return + } + + var results: SearchResultList + do { + results = try JSONDecoder().decode(SearchResultList.self, from: data) + } catch { + completion(nil, MASError.jsonParsing(error: error as NSError)) + return } - group.enter() - scrapeVersionFromPage(pageUrl) { pageVersion in - if let pageVersion = pageVersion, pageVersion > searchVersion { - results.results[index].version = pageVersion.description + let group = DispatchGroup() + for index in results.results.indices { + let result = results.results[index] + guard let searchVersion = Version(tolerant: result.version), + let pageUrl = URL(string: result.trackViewUrl) + else { + continue } - group.leave() - } - } + group.enter() + self.scrapeVersionFromPage(pageUrl) { pageVersion in + if let pageVersion = pageVersion, pageVersion > searchVersion { + results.results[index].version = pageVersion.description + } - group.wait() + group.leave() + } + } - return results + group.notify(queue: DispatchQueue.global()) { + completion(results, nil) + } + } } // The App Store often lists a newer version available in an app's page than in diff --git a/MasKit/Controllers/StoreSearch.swift b/MasKit/Controllers/StoreSearch.swift index f7e817b43..2a22fda86 100644 --- a/MasKit/Controllers/StoreSearch.swift +++ b/MasKit/Controllers/StoreSearch.swift @@ -10,12 +10,64 @@ import Foundation /// Protocol for searching the MAS catalog. public protocol StoreSearch { - func lookup(app appId: Int) throws -> SearchResult? - func search(for appName: String) throws -> SearchResultList + func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) + func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) } // MARK: - Common methods extension StoreSearch { + /// 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: Int) throws -> SearchResult? { + var result: SearchResult? + var error: Error? + + let group = DispatchGroup() + group.enter() + lookup(app: appId) { + result = $0 + error = $1 + group.leave() + } + + group.wait() + + if let error = error { + throw error + } + + return result + } + + /// 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 { + var results: SearchResultList? + var error: Error? + + let group = DispatchGroup() + group.enter() + search(for: appName) { + results = $0 + error = $1 + group.leave() + } + + group.wait() + + if let error = error { + throw error + } + + return results! + } + /// Builds the search URL for an app. /// /// - Parameter appName: MAS app identifier. diff --git a/MasKitTests/Controllers/StoreSearchMock.swift b/MasKitTests/Controllers/StoreSearchMock.swift index e899e65e5..bb9e45a29 100644 --- a/MasKitTests/Controllers/StoreSearchMock.swift +++ b/MasKitTests/Controllers/StoreSearchMock.swift @@ -11,21 +11,26 @@ class StoreSearchMock: StoreSearch { var apps: [Int: SearchResult] = [:] - func search(for appName: String) throws -> SearchResultList { + func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) { let filtered = apps.filter { $1.trackName.contains(appName) } - return SearchResultList(resultCount: filtered.count, results: filtered.map { $1 }) + let results = SearchResultList(resultCount: filtered.count, results: filtered.map { $1 }) + completion(results, nil) } - func lookup(app appId: Int) throws -> SearchResult? { + func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) { // Negative numbers are invalid - if appId <= 0 { - throw MASError.searchFailed + guard appId > 0 else { + completion(nil, MASError.searchFailed) + return } guard let result = apps[appId] - else { throw MASError.noSearchResultsFound } + else { + completion(nil, MASError.noSearchResultsFound) + return + } - return result + completion(result, nil) } func reset() { diff --git a/MasKitTests/Controllers/StoreSearchSpec.swift b/MasKitTests/Controllers/StoreSearchSpec.swift index 87b219e62..cbc54bf97 100644 --- a/MasKitTests/Controllers/StoreSearchSpec.swift +++ b/MasKitTests/Controllers/StoreSearchSpec.swift @@ -12,8 +12,13 @@ import Quick /// Protocol minimal implementation struct StoreSearchForTesting: StoreSearch { - func lookup(app _: Int) throws -> SearchResult? { nil } - func search(for _: String) throws -> SearchResultList { SearchResultList(resultCount: 0, results: []) } + func lookup(app _: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) { + completion(nil, nil) + } + + func search(for _: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) { + completion(SearchResultList(resultCount: 0, results: []), nil) + } } class StoreSearchSpec: QuickSpec { From 5f9fc870df5d7731ab68bb6c458ddaa4fd36a02a Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 15:53:36 -0700 Subject: [PATCH 15/24] =?UTF-8?q?=F0=9F=A4=AB=20Silence=20remaining=20lint?= =?UTF-8?q?=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .swift-format | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.swift-format b/.swift-format index d9da89a8a..9c12fd605 100644 --- a/.swift-format +++ b/.swift-format @@ -7,7 +7,7 @@ "AllPublicDeclarationsHaveDocumentation" : false, "AlwaysUseLowerCamelCase" : true, "AmbiguousTrailingClosureOverload" : true, - "BeginDocumentationCommentWithOneLineSummary" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, "DoNotUseSemicolons" : true, "DontRepeatTypeInStaticProperties" : true, "FileScopedDeclarationPrivacy" : true, From 2d96ca852f9b26e406f15c88f0a196b1a8bcab2f Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 15:58:48 -0700 Subject: [PATCH 16/24] =?UTF-8?q?=F0=9F=8F=83=20Run=20outdated=20faster=20?= =?UTF-8?q?by=20issuing=20multiple=20searches=20at=20the=20same=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Commands/Outdated.swift | 42 +++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/MasKit/Commands/Outdated.swift b/MasKit/Commands/Outdated.swift index 7179a7b98..e6fe95233 100644 --- a/MasKit/Commands/Outdated.swift +++ b/MasKit/Commands/Outdated.swift @@ -33,28 +33,44 @@ public struct OutdatedCommand: CommandProtocol { /// Runs the command. public func run(_: Options) -> Result { + var failure: MASError? + let group = DispatchGroup() for installedApp in appLibrary.installedApps { - do { - if let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue) { - if installedApp.isOutdatedWhenComparedTo(storeApp) { - print( - """ - \(installedApp.itemIdentifier) \(installedApp.appName) \ - (\(installedApp.bundleVersion) -> \(storeApp.version)) - """) - } - } else { + group.enter() + storeSearch.lookup(app: installedApp.itemIdentifier.intValue) { storeApp, error in + defer { group.leave() } + + if let error = error { + // Bubble up MASErrors + failure = error as? MASError ?? .searchFailed + return + } + + guard let storeApp = storeApp else { printWarning( """ Identifier \(installedApp.itemIdentifier) not found in store. \ Was expected to identify \(installedApp.appName). """) + return + } + + if installedApp.isOutdatedWhenComparedTo(storeApp) { + print( + """ + \(installedApp.itemIdentifier) \(installedApp.appName) \ + (\(installedApp.bundleVersion) -> \(storeApp.version)) + """) } - } catch { - // Bubble up MASErrors - return .failure(error as? MASError ?? .searchFailed) } } + + group.wait() + + if let failure = failure { + return .failure(failure) + } + return .success(()) } } From f3bfc1a1051455d0be0cf44974c9a4eb5dde45a2 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 16:09:14 -0700 Subject: [PATCH 17/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20Upgrade?= =?UTF-8?q?=20to=20improve=20readability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Commands/Upgrade.swift | 98 ++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/MasKit/Commands/Upgrade.swift b/MasKit/Commands/Upgrade.swift index 5a4b15fd2..758fa3d0f 100644 --- a/MasKit/Commands/Upgrade.swift +++ b/MasKit/Commands/Upgrade.swift @@ -32,64 +32,66 @@ public struct UpgradeCommand: CommandProtocol { /// Runs the command. public func run(_ options: Options) -> Result { - do { - let apps = - try - (options.apps.count == 0 - ? appLibrary.installedApps - : options.apps.compactMap { - if let appId = UInt64($0) { - // if argument a UInt64, lookup app by id using argument - return appLibrary.installedApp(forId: appId) - } else { - // if argument not a UInt64, lookup app by name using argument - return appLibrary.installedApp(named: $0) - } - }) - .compactMap { (installedApp: SoftwareProduct) -> SoftwareProduct? in - // only upgrade apps whose local version differs from the store version - if let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue) { - return installedApp.isOutdatedWhenComparedTo(storeApp) - ? installedApp - : nil - } else { - return nil - } - } - - guard apps.count > 0 else { - printWarning("Nothing found to upgrade") - return .success(()) - } - - print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") - print(apps.map { "\($0.appName) (\($0.bundleVersion))" }.joined(separator: ", ")) - - var updatedAppCount = 0 - var failedUpgradeResults = [MASError]() - for app in apps { - if let upgradeResult = download(app.itemIdentifier.uint64Value) { - failedUpgradeResults.append(upgradeResult) + var apps: [SoftwareProduct] + if options.apps.isEmpty { + apps = appLibrary.installedApps + } else { + apps = options.apps.compactMap { + if let appId = UInt64($0) { + // if argument a UInt64, lookup app by id using argument + return appLibrary.installedApp(forId: appId) } else { - updatedAppCount += 1 + // if argument not a UInt64, lookup app by name using argument + return appLibrary.installedApp(named: $0) } } + } - switch failedUpgradeResults.count { - case 0: - if updatedAppCount == 0 { - print("Everything is up-to-date") + do { + apps = try apps.compactMap { installedApp in + // only upgrade apps whose local version differs from the store version + guard let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue), + installedApp.isOutdatedWhenComparedTo(storeApp) + else { + return nil } - return .success(()) - case 1: - return .failure(failedUpgradeResults[0]) - default: - return .failure(.downloadFailed(error: nil)) + + return installedApp } } catch { // Bubble up MASErrors return .failure(error as? MASError ?? .searchFailed) } + + guard apps.count > 0 else { + printWarning("Nothing found to upgrade") + return .success(()) + } + + print("Upgrading \(apps.count) outdated application\(apps.count > 1 ? "s" : ""):") + print(apps.map { "\($0.appName) (\($0.bundleVersion))" }.joined(separator: ", ")) + + var updatedAppCount = 0 + var failedUpgradeResults = [MASError]() + for app in apps { + if let upgradeResult = download(app.itemIdentifier.uint64Value) { + failedUpgradeResults.append(upgradeResult) + } else { + updatedAppCount += 1 + } + } + + switch failedUpgradeResults.count { + case 0: + if updatedAppCount == 0 { + print("Everything is up-to-date") + } + return .success(()) + case 1: + return .failure(failedUpgradeResults[0]) + default: + return .failure(.downloadFailed(error: nil)) + } } } From 9b24cc8d49c24fa7d4560ba41754c1fa3b94aa42 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 16:15:26 -0700 Subject: [PATCH 18/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extract=20findOutdat?= =?UTF-8?q?edApps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Commands/Upgrade.swift | 57 ++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/MasKit/Commands/Upgrade.swift b/MasKit/Commands/Upgrade.swift index 758fa3d0f..fa7f99ec7 100644 --- a/MasKit/Commands/Upgrade.swift +++ b/MasKit/Commands/Upgrade.swift @@ -32,32 +32,9 @@ public struct UpgradeCommand: CommandProtocol { /// Runs the command. public func run(_ options: Options) -> Result { - var apps: [SoftwareProduct] - if options.apps.isEmpty { - apps = appLibrary.installedApps - } else { - apps = options.apps.compactMap { - if let appId = UInt64($0) { - // if argument a UInt64, lookup app by id using argument - return appLibrary.installedApp(forId: appId) - } else { - // if argument not a UInt64, lookup app by name using argument - return appLibrary.installedApp(named: $0) - } - } - } - + let apps: [SoftwareProduct] do { - apps = try apps.compactMap { installedApp in - // only upgrade apps whose local version differs from the store version - guard let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue), - installedApp.isOutdatedWhenComparedTo(storeApp) - else { - return nil - } - - return installedApp - } + apps = try findOutdatedApps(options) } catch { // Bubble up MASErrors return .failure(error as? MASError ?? .searchFailed) @@ -93,6 +70,36 @@ public struct UpgradeCommand: CommandProtocol { return .failure(.downloadFailed(error: nil)) } } + + private func findOutdatedApps(_ options: Options) throws -> [SoftwareProduct] { + var apps: [SoftwareProduct] + if options.apps.isEmpty { + apps = appLibrary.installedApps + } else { + apps = options.apps.compactMap { + if let appId = UInt64($0) { + // if argument a UInt64, lookup app by id using argument + return appLibrary.installedApp(forId: appId) + } else { + // if argument not a UInt64, lookup app by name using argument + return appLibrary.installedApp(named: $0) + } + } + } + + apps = try apps.compactMap { installedApp in + // only upgrade apps whose local version differs from the store version + guard let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue), + installedApp.isOutdatedWhenComparedTo(storeApp) + else { + return nil + } + + return installedApp + } + + return apps + } } public struct UpgradeOptions: OptionsProtocol { From 7f37916e41f7176ba410da8a3e61958a5b48bd06 Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 16:33:51 -0700 Subject: [PATCH 19/24] =?UTF-8?q?=F0=9F=8F=83=20Run=20upgrade=20faster=20b?= =?UTF-8?q?y=20issuing=20multiple=20searches=20at=20the=20same=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Commands/Upgrade.swift | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/MasKit/Commands/Upgrade.swift b/MasKit/Commands/Upgrade.swift index fa7f99ec7..baf09c259 100644 --- a/MasKit/Commands/Upgrade.swift +++ b/MasKit/Commands/Upgrade.swift @@ -87,18 +87,27 @@ public struct UpgradeCommand: CommandProtocol { } } - apps = try apps.compactMap { installedApp in + var outdated = [SoftwareProduct]() + let group = DispatchGroup() + let semaphore = DispatchSemaphore(value: 1) + for installedApp in apps { // only upgrade apps whose local version differs from the store version - guard let storeApp = try storeSearch.lookup(app: installedApp.itemIdentifier.intValue), - installedApp.isOutdatedWhenComparedTo(storeApp) - else { - return nil - } + group.enter() + storeSearch.lookup(app: installedApp.itemIdentifier.intValue) { result, _ in + defer { group.leave() } + + if let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) { + semaphore.wait() + defer { semaphore.signal() } - return installedApp + outdated.append(installedApp) + } + } } - return apps + group.wait() + + return outdated } } From 86a9fccb02ba582b4114ce33d4589b63d5fd5c0f Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 17:04:44 -0700 Subject: [PATCH 20/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Simplify=20network?= =?UTF-8?q?=20closures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 12 +++--- MasKit/Network/NetworkManager.swift | 37 ++++++------------- MasKit/Network/NetworkResult.swift | 29 --------------- MasKitTests/Network/NetworkManagerTests.swift | 31 +++++++++++----- mas-cli.xcodeproj/project.pbxproj | 4 -- 5 files changed, 39 insertions(+), 74 deletions(-) delete mode 100644 MasKit/Network/NetworkResult.swift diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 26ab16a30..9831ec644 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -70,9 +70,9 @@ public class MasStoreSearch: StoreSearch { } public func loadSearchResults(_ url: URL, _ completion: @escaping (SearchResultList?, Error?) -> Void) { - networkManager.loadData(from: url) { result in - guard case let .success(data) = result else { - if case let .failure(error) = result { + networkManager.loadData(from: url) { data, error in + guard let data = data else { + if let error = error { completion(nil, error) } else { completion(nil, MASError.noData) @@ -117,13 +117,13 @@ public class MasStoreSearch: StoreSearch { // The App Store often lists a newer version available in an app's page than in // the search results. We attempt to scrape it here. private func scrapeVersionFromPage(_ pageUrl: URL, _ completion: @escaping (Version?) -> Void) { - networkManager.loadData(from: pageUrl) { result in - guard case let .success(pageData) = result else { + networkManager.loadData(from: pageUrl) { data, _ in + guard let data = data else { completion(nil) return } - let html = String(decoding: pageData, as: UTF8.self) + let html = String(decoding: data, as: UTF8.self) let fullRange = NSRange(html.startIndex.. Void) { - session.loadData(from: url) { (data: Data?, error: Error?) in - let result: NetworkResult = - data != nil - ? .success(data!) - : .failure(error!) - completionHandler(result) - } + func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) { + session.loadData(from: url, completionHandler: completionHandler) } /// Loads data synchronously. @@ -49,29 +39,26 @@ public class NetworkManager { /// - Parameter url: URL to load data from. /// - Returns: The Data of the response. func loadDataSync(from url: URL) throws -> Data { - var syncResult: NetworkResult? + var data: Data? + var error: Error? let group = DispatchGroup() group.enter() - loadData(from: url) { asyncResult in - syncResult = asyncResult + session.loadData(from: url) { + data = $0 + error = $1 group.leave() } group.wait() - guard let result = syncResult else { - throw NetworkError.timeout + if let error = error { + throw error } - // Unwrap network result - guard case let .success(data) = result - else { - if case let .failure(error) = result { - throw error - } - throw MASError.noData + if let data = data { + return data } - return data + throw MASError.noData } } diff --git a/MasKit/Network/NetworkResult.swift b/MasKit/Network/NetworkResult.swift deleted file mode 100644 index eadc3e02f..000000000 --- a/MasKit/Network/NetworkResult.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// NetworkResult.swift -// MasKit -// -// Created by Ben Chatelain on 1/5/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -import Foundation - -enum NetworkResult { - case success(Data) - case failure(Error) -} - -extension NetworkResult: Equatable { - static func == (lhs: NetworkResult, rhs: NetworkResult) -> Bool { - switch (lhs, rhs) { - case (.success(let data1), .success(let data2)): - return data1 == data2 - - case (.failure(let error1), .failure(let error2)): - return error1.localizedDescription == error2.localizedDescription - - default: - return false - } - } -} diff --git a/MasKitTests/Network/NetworkManagerTests.swift b/MasKitTests/Network/NetworkManagerTests.swift index 259135773..c78690c8a 100644 --- a/MasKitTests/Network/NetworkManagerTests.swift +++ b/MasKitTests/Network/NetworkManagerTests.swift @@ -24,9 +24,15 @@ class NetworkManagerTests: XCTestCase { 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)) + var response: Data? + var error: Error? + manager.loadData(from: url) { + response = $0 + error = $1 + } + + XCTAssertEqual(response, data) + XCTAssertNil(error) } func testSuccessfulSyncResponse() throws { @@ -51,15 +57,20 @@ class NetworkManagerTests: XCTestCase { let session = NetworkSessionMock() let manager = NetworkManager(session: session) - session.error = NetworkManager.NetworkError.timeout + session.error = MASError.noData // 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)) + var error: Error! + manager.loadData(from: url) { error = $1 } + guard let masError = error as? MASError else { + XCTFail("Error is of unexpected type.") + return + } + + XCTAssertEqual(masError, MASError.noData) } func testFailureSyncResponse() { @@ -67,19 +78,19 @@ class NetworkManagerTests: XCTestCase { let session = NetworkSessionMock() let manager = NetworkManager(session: session) - session.error = NetworkManager.NetworkError.timeout + session.error = MASError.noData // Create a URL (using the file path API to avoid optionals) let url = URL(fileURLWithPath: "url") // Perform the request and verify the result XCTAssertThrowsError(try manager.loadDataSync(from: url)) { error in - guard let error = error as? NetworkManager.NetworkError else { + guard let error = error as? MASError else { XCTFail("Error is of unexpected type.") return } - XCTAssertEqual(error, NetworkManager.NetworkError.timeout) + XCTAssertEqual(error, MASError.noData) } } } diff --git a/mas-cli.xcodeproj/project.pbxproj b/mas-cli.xcodeproj/project.pbxproj index 3e96bfaf5..ea3e8b041 100644 --- a/mas-cli.xcodeproj/project.pbxproj +++ b/mas-cli.xcodeproj/project.pbxproj @@ -28,7 +28,6 @@ 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 */; }; @@ -215,7 +214,6 @@ B576FDFF21E113610016B39D /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = ""; }; B576FE0121E1139E0016B39D /* URLSession+NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLSession+NetworkSession.swift"; sourceTree = ""; }; B576FE0321E113E90016B39D /* NetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManager.swift; sourceTree = ""; }; - B576FE0721E114A80016B39D /* NetworkResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResult.swift; sourceTree = ""; }; B576FE0B21E116590016B39D /* NetworkManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerTests.swift; sourceTree = ""; }; B576FE0D21E1D6310016B39D /* String+PercentEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+PercentEncoding.swift"; sourceTree = ""; }; B576FE1121E1D82D0016B39D /* NetworkSessionMockFromFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkSessionMockFromFile.swift; sourceTree = ""; }; @@ -381,7 +379,6 @@ isa = PBXGroup; children = ( B576FE0321E113E90016B39D /* NetworkManager.swift */, - B576FE0721E114A80016B39D /* NetworkResult.swift */, B576FDFF21E113610016B39D /* NetworkSession.swift */, B576FE0121E1139E0016B39D /* URLSession+NetworkSession.swift */, ); @@ -945,7 +942,6 @@ F8FB717B20F2B4DD00F56FDC /* MASError.swift in Sources */, B594B15221D89A8B00F3AC59 /* MasStoreSearch.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 */, From 6b8e91b13613ef61d159194133f0082e506f43cf Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 17:25:35 -0700 Subject: [PATCH 21/24] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Pass=20around=20simp?= =?UTF-8?q?le=20array=20of=20SearchResult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Commands/Lucky.swift | 2 +- MasKit/Commands/Search.swift | 6 +++--- MasKit/Controllers/MasStoreSearch.swift | 10 +++++----- MasKit/Controllers/StoreSearch.swift | 8 ++++---- MasKitTests/Controllers/MasStoreSearchSpec.swift | 7 +++---- MasKitTests/Controllers/StoreSearchMock.swift | 4 ++-- MasKitTests/Controllers/StoreSearchSpec.swift | 4 ++-- 7 files changed, 20 insertions(+), 21 deletions(-) diff --git a/MasKit/Commands/Lucky.swift b/MasKit/Commands/Lucky.swift index 4c9f4f063..f54a3e769 100644 --- a/MasKit/Commands/Lucky.swift +++ b/MasKit/Commands/Lucky.swift @@ -42,7 +42,7 @@ public struct LuckyCommand: CommandProtocol { do { let results = try storeSearch.search(for: options.appName) - guard let result = results.results.first else { + guard let result = results.first else { print("No results found") return .failure(.noSearchResultsFound) } diff --git a/MasKit/Commands/Search.swift b/MasKit/Commands/Search.swift index f3bd86f49..f80879148 100644 --- a/MasKit/Commands/Search.swift +++ b/MasKit/Commands/Search.swift @@ -26,13 +26,13 @@ public struct SearchCommand: CommandProtocol { public func run(_ options: Options) -> Result { do { - let resultList = try storeSearch.search(for: options.appName) - if resultList.resultCount <= 0 || resultList.results.isEmpty { + let results = try storeSearch.search(for: options.appName) + if results.isEmpty { print("No results found") return .failure(.noSearchResultsFound) } - let output = SearchResultFormatter.format(results: resultList.results, includePrice: options.price) + let output = SearchResultFormatter.format(results: results, includePrice: options.price) print(output) return .success(()) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 9831ec644..937fb4f18 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -29,8 +29,8 @@ public class MasStoreSearch: StoreSearch { /// /// - Parameter appName: MAS ID of app /// - Parameter completion: A closure that receives the search results or an Error if there is a - /// problem with the network request. List will have no records if there were no matches. - public func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) { + /// problem with the network request. Results array will be empty if there were no matches. + public func search(for appName: String, _ completion: @escaping ([SearchResult]?, Error?) -> Void) { guard let url = searchURL(for: appName) else { completion(nil, MASError.urlEncoding) @@ -65,11 +65,11 @@ public class MasStoreSearch: StoreSearch { return } - completion(results?.results.first, nil) + completion(results?.first, nil) } } - public func loadSearchResults(_ url: URL, _ completion: @escaping (SearchResultList?, Error?) -> Void) { + public func loadSearchResults(_ url: URL, _ completion: @escaping ([SearchResult]?, Error?) -> Void) { networkManager.loadData(from: url) { data, error in guard let data = data else { if let error = error { @@ -109,7 +109,7 @@ public class MasStoreSearch: StoreSearch { } group.notify(queue: DispatchQueue.global()) { - completion(results, nil) + completion(results.results, nil) } } } diff --git a/MasKit/Controllers/StoreSearch.swift b/MasKit/Controllers/StoreSearch.swift index 2a22fda86..d72b57d29 100644 --- a/MasKit/Controllers/StoreSearch.swift +++ b/MasKit/Controllers/StoreSearch.swift @@ -11,7 +11,7 @@ import Foundation /// Protocol for searching the MAS catalog. public protocol StoreSearch { func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) - func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) + func search(for appName: String, _ completion: @escaping ([SearchResult]?, Error?) -> Void) } // MARK: - Common methods @@ -45,10 +45,10 @@ extension StoreSearch { /// 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. + /// - Returns: Search results. Empty if there were no matches. /// - Throws: Error if there is a problem with the network request. - public func search(for appName: String) throws -> SearchResultList { - var results: SearchResultList? + public func search(for appName: String) throws -> [SearchResult] { + var results: [SearchResult]? var error: Error? let group = DispatchGroup() diff --git a/MasKitTests/Controllers/MasStoreSearchSpec.swift b/MasKitTests/Controllers/MasStoreSearchSpec.swift index 176182b84..a1e5ccc3b 100644 --- a/MasKitTests/Controllers/MasStoreSearchSpec.swift +++ b/MasKitTests/Controllers/MasStoreSearchSpec.swift @@ -19,11 +19,10 @@ class MasStoreSearchSpec: QuickSpec { let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) - var searchList: SearchResultList + var results: [SearchResult] do { - searchList = try storeSearch.search(for: "slack") - expect(searchList.resultCount) == 39 - expect(searchList.results.count) == 39 + results = try storeSearch.search(for: "slack") + expect(results.count) == 39 } catch { let maserror = error as! MASError if case let .jsonParsing(nserror) = maserror { diff --git a/MasKitTests/Controllers/StoreSearchMock.swift b/MasKitTests/Controllers/StoreSearchMock.swift index bb9e45a29..20a479211 100644 --- a/MasKitTests/Controllers/StoreSearchMock.swift +++ b/MasKitTests/Controllers/StoreSearchMock.swift @@ -11,9 +11,9 @@ class StoreSearchMock: StoreSearch { var apps: [Int: SearchResult] = [:] - func search(for appName: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) { + func search(for appName: String, _ completion: @escaping ([SearchResult]?, Error?) -> Void) { let filtered = apps.filter { $1.trackName.contains(appName) } - let results = SearchResultList(resultCount: filtered.count, results: filtered.map { $1 }) + let results = filtered.map { $1 } completion(results, nil) } diff --git a/MasKitTests/Controllers/StoreSearchSpec.swift b/MasKitTests/Controllers/StoreSearchSpec.swift index cbc54bf97..dd90ef323 100644 --- a/MasKitTests/Controllers/StoreSearchSpec.swift +++ b/MasKitTests/Controllers/StoreSearchSpec.swift @@ -16,8 +16,8 @@ struct StoreSearchForTesting: StoreSearch { completion(nil, nil) } - func search(for _: String, _ completion: @escaping (SearchResultList?, Error?) -> Void) { - completion(SearchResultList(resultCount: 0, results: []), nil) + func search(for _: String, _ completion: @escaping ([SearchResult]?, Error?) -> Void) { + completion([], nil) } } From a27cc68d2c35a52f0940fbc05f81e2901be2b55c Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 18:05:36 -0700 Subject: [PATCH 22/24] =?UTF-8?q?=F0=9F=99=88=20hide=20unnecessary=20publi?= =?UTF-8?q?cs=20and=20@objc=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Commands/Home.swift | 9 +++++- MasKit/Commands/Info.swift | 6 +++- MasKit/Commands/Lucky.swift | 8 ++++-- MasKit/Commands/Open.swift | 9 +++++- MasKit/Commands/Search.swift | 6 +++- MasKit/Commands/Vendor.swift | 9 +++++- MasKit/Controllers/AppLibrary.swift | 6 ++-- MasKit/Controllers/MasAppLibrary.swift | 8 +++--- MasKit/Controllers/MasStoreSearch.swift | 10 +++---- MasKit/Controllers/StoreSearch.swift | 10 +++---- MasKit/ExternalCommands/ExternalCommand.swift | 14 +++++----- .../ExternalCommands/OpenSystemCommand.swift | 14 ++++------ MasKit/Models/SearchResult.swift | 28 +++++++++---------- MasKit/Models/SearchResultList.swift | 2 +- MasKit/Models/SoftwareProduct.swift | 2 +- MasKit/Network/NetworkManager.swift | 4 +-- MasKit/Network/NetworkSession.swift | 4 +-- .../Network/URLSession+NetworkSession.swift | 2 +- MasKitTests/Controllers/AppLibraryMock.swift | 2 +- MasKitTests/Network/NetworkSessionMock.swift | 4 +-- .../Network/NetworkSessionMockFromFile.swift | 2 +- mas/main.swift | 4 +-- 22 files changed, 97 insertions(+), 66 deletions(-) diff --git a/MasKit/Commands/Home.swift b/MasKit/Commands/Home.swift index 47abe2805..2caf200b2 100644 --- a/MasKit/Commands/Home.swift +++ b/MasKit/Commands/Home.swift @@ -19,8 +19,15 @@ public struct HomeCommand: CommandProtocol { private let storeSearch: StoreSearch private var openCommand: ExternalCommand + public init() { + self.init( + storeSearch: MasStoreSearch(), + openCommand: OpenSystemCommand() + ) + } + /// Designated initializer. - public init( + init( storeSearch: StoreSearch = MasStoreSearch(), openCommand: ExternalCommand = OpenSystemCommand() ) { diff --git a/MasKit/Commands/Info.swift b/MasKit/Commands/Info.swift index 61a31852d..26f3221fc 100644 --- a/MasKit/Commands/Info.swift +++ b/MasKit/Commands/Info.swift @@ -17,8 +17,12 @@ public struct InfoCommand: CommandProtocol { private let storeSearch: StoreSearch + public init() { + self.init(storeSearch: MasStoreSearch()) + } + /// Designated initializer. - public init(storeSearch: StoreSearch = MasStoreSearch()) { + init(storeSearch: StoreSearch = MasStoreSearch()) { self.storeSearch = storeSearch } diff --git a/MasKit/Commands/Lucky.swift b/MasKit/Commands/Lucky.swift index f54a3e769..97f30f831 100644 --- a/MasKit/Commands/Lucky.swift +++ b/MasKit/Commands/Lucky.swift @@ -19,9 +19,13 @@ public struct LuckyCommand: CommandProtocol { private let appLibrary: AppLibrary private let storeSearch: StoreSearch - /// Public initializer. + public init() { + self.init(storeSearch: MasStoreSearch()) + } + + /// Designated initializer. /// - Parameter storeSearch: Search manager. - public init(storeSearch: StoreSearch = MasStoreSearch()) { + init(storeSearch: StoreSearch = MasStoreSearch()) { self.init(appLibrary: MasAppLibrary(), storeSearch: storeSearch) } diff --git a/MasKit/Commands/Open.swift b/MasKit/Commands/Open.swift index 5c00ac84d..732c1448a 100644 --- a/MasKit/Commands/Open.swift +++ b/MasKit/Commands/Open.swift @@ -22,8 +22,15 @@ public struct OpenCommand: CommandProtocol { private let storeSearch: StoreSearch private var systemOpen: ExternalCommand + public init() { + self.init( + storeSearch: MasStoreSearch(), + openCommand: OpenSystemCommand() + ) + } + /// Designated initializer. - public init( + init( storeSearch: StoreSearch = MasStoreSearch(), openCommand: ExternalCommand = OpenSystemCommand() ) { diff --git a/MasKit/Commands/Search.swift b/MasKit/Commands/Search.swift index f80879148..627211840 100644 --- a/MasKit/Commands/Search.swift +++ b/MasKit/Commands/Search.swift @@ -17,10 +17,14 @@ public struct SearchCommand: CommandProtocol { private let storeSearch: StoreSearch + public init() { + self.init(storeSearch: MasStoreSearch()) + } + /// Designated initializer. /// /// - Parameter storeSearch: Search manager. - public init(storeSearch: StoreSearch = MasStoreSearch()) { + init(storeSearch: StoreSearch = MasStoreSearch()) { self.storeSearch = storeSearch } diff --git a/MasKit/Commands/Vendor.swift b/MasKit/Commands/Vendor.swift index 8fa0ce51b..38bb56db6 100644 --- a/MasKit/Commands/Vendor.swift +++ b/MasKit/Commands/Vendor.swift @@ -19,8 +19,15 @@ public struct VendorCommand: CommandProtocol { private let storeSearch: StoreSearch private var openCommand: ExternalCommand + public init() { + self.init( + storeSearch: MasStoreSearch(), + openCommand: OpenSystemCommand() + ) + } + /// Designated initializer. - public init( + init( storeSearch: StoreSearch = MasStoreSearch(), openCommand: ExternalCommand = OpenSystemCommand() ) { diff --git a/MasKit/Controllers/AppLibrary.swift b/MasKit/Controllers/AppLibrary.swift index 998812e1f..c02bf40d4 100644 --- a/MasKit/Controllers/AppLibrary.swift +++ b/MasKit/Controllers/AppLibrary.swift @@ -9,7 +9,7 @@ import Foundation /// Utility for managing installed apps. -public protocol AppLibrary { +protocol AppLibrary { /// Entire set of installed apps. var installedApps: [SoftwareProduct] { get } @@ -44,7 +44,7 @@ extension AppLibrary { /// /// - Parameter id: MAS ID for app. /// - Returns: Software Product of app if found; nil otherwise. - public func installedApp(forId identifier: UInt64) -> SoftwareProduct? { + func installedApp(forId identifier: UInt64) -> SoftwareProduct? { let appId = NSNumber(value: identifier) return installedApps.first { $0.itemIdentifier == appId } } @@ -53,7 +53,7 @@ extension AppLibrary { /// /// - Parameter appName: Full title of an app. /// - Returns: Software Product of app if found; nil otherwise. - public func installedApp(named appName: String) -> SoftwareProduct? { + func installedApp(named appName: String) -> SoftwareProduct? { installedApps.first { $0.appName == appName } } } diff --git a/MasKit/Controllers/MasAppLibrary.swift b/MasKit/Controllers/MasAppLibrary.swift index c1bc8fc1d..c87befaa7 100644 --- a/MasKit/Controllers/MasAppLibrary.swift +++ b/MasKit/Controllers/MasAppLibrary.swift @@ -9,12 +9,12 @@ import CommerceKit /// Utility for managing installed apps. -public class MasAppLibrary: AppLibrary { +class MasAppLibrary: AppLibrary { /// CommerceKit's singleton manager of installed software. private let softwareMap: SoftwareMap /// Array of installed software products. - public lazy var installedApps: [SoftwareProduct] = { + lazy var installedApps: [SoftwareProduct] = { softwareMap.allSoftwareProducts() }() @@ -28,7 +28,7 @@ public class MasAppLibrary: AppLibrary { /// /// - Parameter bundleId: Bundle identifier of app. /// - Returns: Software Product of app if found; nil otherwise. - public func installedApp(forBundleId bundleId: String) -> SoftwareProduct? { + func installedApp(forBundleId bundleId: String) -> SoftwareProduct? { softwareMap.product(for: bundleId) } @@ -36,7 +36,7 @@ public class MasAppLibrary: AppLibrary { /// /// - Parameter app: App to be removed. /// - Throws: Error if there is a problem. - public func uninstallApp(app: SoftwareProduct) throws { + func uninstallApp(app: SoftwareProduct) throws { if !userIsRoot() { printWarning("Apps installed from the Mac App Store require root permission to remove.") } diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 937fb4f18..1ad31545e 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -10,7 +10,7 @@ import Foundation import Version /// Manages searching the MAS catalog through the iTunes Search and Lookup APIs. -public class MasStoreSearch: StoreSearch { +class MasStoreSearch: StoreSearch { private let networkManager: NetworkManager private static let versionExpression: NSRegularExpression? = { do { @@ -21,7 +21,7 @@ public class MasStoreSearch: StoreSearch { }() /// Designated initializer. - public init(networkManager: NetworkManager = NetworkManager()) { + init(networkManager: NetworkManager = NetworkManager()) { self.networkManager = networkManager } @@ -30,7 +30,7 @@ public class MasStoreSearch: StoreSearch { /// - Parameter appName: MAS ID of app /// - Parameter completion: A closure that receives the search results or an Error if there is a /// problem with the network request. Results array will be empty if there were no matches. - public func search(for appName: String, _ completion: @escaping ([SearchResult]?, Error?) -> Void) { + func search(for appName: String, _ completion: @escaping ([SearchResult]?, Error?) -> Void) { guard let url = searchURL(for: appName) else { completion(nil, MASError.urlEncoding) @@ -52,7 +52,7 @@ public class MasStoreSearch: StoreSearch { /// - Parameter appId: MAS ID of app /// - Parameter completion: A closure that receives the search result record of app, or nil if no apps match the ID, /// or an Error if there is a problem with the network request. - public func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) { + func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) { guard let url = lookupURL(forApp: appId) else { completion(nil, MASError.urlEncoding) @@ -69,7 +69,7 @@ public class MasStoreSearch: StoreSearch { } } - public func loadSearchResults(_ url: URL, _ completion: @escaping ([SearchResult]?, Error?) -> Void) { + private func loadSearchResults(_ url: URL, _ completion: @escaping ([SearchResult]?, Error?) -> Void) { networkManager.loadData(from: url) { data, error in guard let data = data else { if let error = error { diff --git a/MasKit/Controllers/StoreSearch.swift b/MasKit/Controllers/StoreSearch.swift index d72b57d29..9960f53e3 100644 --- a/MasKit/Controllers/StoreSearch.swift +++ b/MasKit/Controllers/StoreSearch.swift @@ -9,7 +9,7 @@ import Foundation /// Protocol for searching the MAS catalog. -public protocol StoreSearch { +protocol StoreSearch { func lookup(app appId: Int, _ completion: @escaping (SearchResult?, Error?) -> Void) func search(for appName: String, _ completion: @escaping ([SearchResult]?, Error?) -> Void) } @@ -21,7 +21,7 @@ extension StoreSearch { /// - 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: Int) throws -> SearchResult? { + func lookup(app appId: Int) throws -> SearchResult? { var result: SearchResult? var error: Error? @@ -47,7 +47,7 @@ extension StoreSearch { /// - Parameter appName: MAS ID of app /// - Returns: Search results. Empty if there were no matches. /// - Throws: Error if there is a problem with the network request. - public func search(for appName: String) throws -> [SearchResult] { + func search(for appName: String) throws -> [SearchResult] { var results: [SearchResult]? var error: Error? @@ -72,7 +72,7 @@ extension StoreSearch { /// /// - 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? { + func searchURL(for appName: String) -> URL? { guard let urlString = searchURLString(forApp: appName) else { return nil } return URL(string: urlString) } @@ -92,7 +92,7 @@ extension StoreSearch { /// /// - Parameter appId: MAS app identifier. /// - Returns: URL for the lookup service or nil if appId can't be encoded. - public func lookupURL(forApp appId: Int) -> URL? { + func lookupURL(forApp appId: Int) -> URL? { guard let urlString = lookupURLString(forApp: appId) else { return nil } return URL(string: urlString) } diff --git a/MasKit/ExternalCommands/ExternalCommand.swift b/MasKit/ExternalCommands/ExternalCommand.swift index 21f14d68e..785bc842c 100644 --- a/MasKit/ExternalCommands/ExternalCommand.swift +++ b/MasKit/ExternalCommands/ExternalCommand.swift @@ -9,7 +9,7 @@ import Foundation /// CLI command -public protocol ExternalCommand { +protocol ExternalCommand { var binaryPath: String { get set } var process: Process { get } @@ -29,30 +29,30 @@ public protocol ExternalCommand { /// Common implementation extension ExternalCommand { - public var stdout: String { + var stdout: String { let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile() return String(data: data, encoding: .utf8) ?? "" } - public var stderr: String { + var stderr: String { let data = stderrPipe.fileHandleForReading.readDataToEndOfFile() return String(data: data, encoding: .utf8) ?? "" } - public var exitCode: Int32 { + var exitCode: Int32 { process.terminationStatus } - public var succeeded: Bool { + var succeeded: Bool { process.terminationReason == .exit && exitCode == 0 } - public var failed: Bool { + var failed: Bool { !succeeded } /// Runs the command. - public func run(arguments: String...) throws { + func run(arguments: String...) throws { process.standardOutput = stdoutPipe process.standardError = stderrPipe process.arguments = arguments diff --git a/MasKit/ExternalCommands/OpenSystemCommand.swift b/MasKit/ExternalCommands/OpenSystemCommand.swift index 81ed687a7..eddb2d639 100644 --- a/MasKit/ExternalCommands/OpenSystemCommand.swift +++ b/MasKit/ExternalCommands/OpenSystemCommand.swift @@ -10,17 +10,15 @@ import Foundation /// Wrapper for the external open system command. /// https://ss64.com/osx/open.html -public struct OpenSystemCommand: ExternalCommand { - public var binaryPath: String +struct OpenSystemCommand: ExternalCommand { + var binaryPath: String - public let process = Process() + let process = Process() - public let stdoutPipe = Pipe() - public let stderrPipe = Pipe() + let stdoutPipe = Pipe() + let stderrPipe = Pipe() - public init( - binaryPath: String = "/usr/bin/open" - ) { + init(binaryPath: String = "/usr/bin/open") { self.binaryPath = binaryPath } } diff --git a/MasKit/Models/SearchResult.swift b/MasKit/Models/SearchResult.swift index 21fd43c9c..303475df0 100644 --- a/MasKit/Models/SearchResult.swift +++ b/MasKit/Models/SearchResult.swift @@ -6,20 +6,20 @@ // 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 +struct SearchResult: Decodable { + var bundleId: String + var currentVersionReleaseDate: String + var fileSizeBytes: String? + var formattedPrice: String? + var minimumOsVersion: String + var price: Double? + var sellerName: String + var sellerUrl: String? + var trackId: Int + var trackCensoredName: String + var trackName: String + var trackViewUrl: String + var version: String init( bundleId: String = "", diff --git a/MasKit/Models/SearchResultList.swift b/MasKit/Models/SearchResultList.swift index d714aeb26..cd29756e4 100644 --- a/MasKit/Models/SearchResultList.swift +++ b/MasKit/Models/SearchResultList.swift @@ -6,7 +6,7 @@ // Copyright © 2018 mas-cli. All rights reserved. // -public struct SearchResultList: Decodable { +struct SearchResultList: Decodable { var resultCount: Int var results: [SearchResult] } diff --git a/MasKit/Models/SoftwareProduct.swift b/MasKit/Models/SoftwareProduct.swift index aa1de2877..de232d3a3 100644 --- a/MasKit/Models/SoftwareProduct.swift +++ b/MasKit/Models/SoftwareProduct.swift @@ -10,7 +10,7 @@ import Foundation import Version /// Protocol describing the members of CKSoftwareProduct used throughout MasKit. -public protocol SoftwareProduct { +protocol SoftwareProduct { var appName: String { get } var bundleIdentifier: String { get set } var bundlePath: String { get set } diff --git a/MasKit/Network/NetworkManager.swift b/MasKit/Network/NetworkManager.swift index 695d8659e..2873d270f 100644 --- a/MasKit/Network/NetworkManager.swift +++ b/MasKit/Network/NetworkManager.swift @@ -9,13 +9,13 @@ import Foundation /// Network abstraction -public class NetworkManager { +class NetworkManager { private let session: NetworkSession /// Designated initializer /// /// - Parameter session: A networking session. - public init(session: NetworkSession = URLSession(configuration: .ephemeral)) { + init(session: NetworkSession = URLSession(configuration: .ephemeral)) { self.session = session // Older releases allowed URLSession to write a cache. We clean it up here. diff --git a/MasKit/Network/NetworkSession.swift b/MasKit/Network/NetworkSession.swift index c54f40ffb..a3ef2cff6 100644 --- a/MasKit/Network/NetworkSession.swift +++ b/MasKit/Network/NetworkSession.swift @@ -8,6 +8,6 @@ import Foundation -@objc public protocol NetworkSession { - @objc func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) +protocol NetworkSession { + func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) } diff --git a/MasKit/Network/URLSession+NetworkSession.swift b/MasKit/Network/URLSession+NetworkSession.swift index 2fce2672b..66c08f5ff 100644 --- a/MasKit/Network/URLSession+NetworkSession.swift +++ b/MasKit/Network/URLSession+NetworkSession.swift @@ -9,7 +9,7 @@ import Foundation extension URLSession: NetworkSession { - @objc open func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) { + open func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) { let task = dataTask(with: url) { data, _, error in completionHandler(data, error) } diff --git a/MasKitTests/Controllers/AppLibraryMock.swift b/MasKitTests/Controllers/AppLibraryMock.swift index 1c250b49f..2290ad994 100644 --- a/MasKitTests/Controllers/AppLibraryMock.swift +++ b/MasKitTests/Controllers/AppLibraryMock.swift @@ -15,7 +15,7 @@ class AppLibraryMock: AppLibrary { /// /// - Parameter bundleId: Bundle identifier of app. /// - Returns: Software Product of app if found; nil otherwise. - public func installedApp(forBundleId _: String) -> SoftwareProduct? { + func installedApp(forBundleId _: String) -> SoftwareProduct? { nil } diff --git a/MasKitTests/Network/NetworkSessionMock.swift b/MasKitTests/Network/NetworkSessionMock.swift index cd43d5687..a3e2f526a 100644 --- a/MasKitTests/Network/NetworkSessionMock.swift +++ b/MasKitTests/Network/NetworkSessionMock.swift @@ -7,7 +7,7 @@ // import Foundation -import MasKit +@testable import MasKit /// Mock NetworkSession for testing. class NetworkSessionMock: NetworkSession { @@ -38,7 +38,7 @@ class NetworkSessionMock: NetworkSession { /// - Parameters: /// - url: unused /// - completionHandler: Closure which is delivered either data or an error. - @objc func loadData(from _: URL, completionHandler: @escaping (Data?, Error?) -> Void) { + func loadData(from _: URL, completionHandler: @escaping (Data?, Error?) -> Void) { completionHandler(data, error) } } diff --git a/MasKitTests/Network/NetworkSessionMockFromFile.swift b/MasKitTests/Network/NetworkSessionMockFromFile.swift index c2bb89038..efe5d129d 100644 --- a/MasKitTests/Network/NetworkSessionMockFromFile.swift +++ b/MasKitTests/Network/NetworkSessionMockFromFile.swift @@ -26,7 +26,7 @@ class NetworkSessionMockFromFile: NetworkSessionMock { /// - Parameters: /// - url: unused /// - completionHandler: Closure which is delivered either data or an error. - @objc override func loadData(from _: URL, completionHandler: @escaping (Data?, Error?) -> Void) { + override func loadData(from _: URL, completionHandler: @escaping (Data?, Error?) -> Void) { guard let fileURL = Bundle.url(for: responseFile) else { fatalError("Unable to load file \(responseFile)") } diff --git a/mas/main.swift b/mas/main.swift index a477332f0..a91e01088 100644 --- a/mas/main.swift +++ b/mas/main.swift @@ -10,8 +10,8 @@ import Commandant import Foundation import MasKit -public struct StderrOutputStream: TextOutputStream { - public mutating func write(_ string: String) { +struct StderrOutputStream: TextOutputStream { + mutating func write(_ string: String) { fputs(string, stderr) } } From 6a12b3096f1658edeb96e5f2340c56c127c1c5ca Mon Sep 17 00:00:00 2001 From: Chris Araman Date: Wed, 21 Apr 2021 22:20:06 -0700 Subject: [PATCH 23/24] =?UTF-8?q?=F0=9F=92=A3=20Fail=20fast=20if=20regular?= =?UTF-8?q?=20expression=20can=20not=20be=20initialized?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Controllers/MasStoreSearch.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MasKit/Controllers/MasStoreSearch.swift b/MasKit/Controllers/MasStoreSearch.swift index 1ad31545e..79d533412 100644 --- a/MasKit/Controllers/MasStoreSearch.swift +++ b/MasKit/Controllers/MasStoreSearch.swift @@ -12,11 +12,11 @@ import Version /// Manages searching the MAS catalog through the iTunes Search and Lookup APIs. class MasStoreSearch: StoreSearch { private let networkManager: NetworkManager - private static let versionExpression: NSRegularExpression? = { + private static let versionExpression: NSRegularExpression = { do { return try NSRegularExpression(pattern: #"\"versionDisplay\"\:\"([^\"]+)\""#) } catch { - return nil + fatalError("Unexpected error initializing NSRegularExpression: \(error.localizedDescription)") } }() @@ -125,7 +125,7 @@ class MasStoreSearch: StoreSearch { let html = String(decoding: data, as: UTF8.self) let fullRange = NSRange(html.startIndex.. Date: Wed, 21 Apr 2021 22:34:09 -0700 Subject: [PATCH 24/24] =?UTF-8?q?=F0=9F=9B=A1=20Rephrase=20as=20guard=20st?= =?UTF-8?q?atements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MasKit/Commands/Outdated.swift | 2 +- MasKit/Network/NetworkManager.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/MasKit/Commands/Outdated.swift b/MasKit/Commands/Outdated.swift index e6fe95233..dbba1e7cb 100644 --- a/MasKit/Commands/Outdated.swift +++ b/MasKit/Commands/Outdated.swift @@ -40,7 +40,7 @@ public struct OutdatedCommand: CommandProtocol { storeSearch.lookup(app: installedApp.itemIdentifier.intValue) { storeApp, error in defer { group.leave() } - if let error = error { + guard error == nil else { // Bubble up MASErrors failure = error as? MASError ?? .searchFailed return diff --git a/MasKit/Network/NetworkManager.swift b/MasKit/Network/NetworkManager.swift index 2873d270f..cb4235467 100644 --- a/MasKit/Network/NetworkManager.swift +++ b/MasKit/Network/NetworkManager.swift @@ -51,14 +51,14 @@ class NetworkManager { group.wait() - if let error = error { - throw error + guard error == nil else { + throw error! } - if let data = data { - return data + guard data != nil else { + throw MASError.noData } - throw MASError.noData + return data! } }