diff --git a/Sources/mas/Controllers/AppStoreSearcher.swift b/Sources/mas/Controllers/AppStoreSearcher.swift index 4e5b65ee3..13e166318 100644 --- a/Sources/mas/Controllers/AppStoreSearcher.swift +++ b/Sources/mas/Controllers/AppStoreSearcher.swift @@ -11,86 +11,15 @@ import PromiseKit /// Protocol for searching the MAS catalog. protocol AppStoreSearcher { - func lookup(appID: AppID) -> Promise - func search(for searchTerm: String) -> Promise<[SearchResult]> -} - -enum Entity: String { - case desktopSoftware - case macSoftware - case iPadSoftware - case iPhoneSoftware = "software" -} - -private enum URLAction { - case lookup - case search - - var queryItemName: String { - switch self { - case .lookup: - return "id" - case .search: - return "term" - } - } -} - -// MARK: - Common methods -extension AppStoreSearcher { - /// Builds the search URL for an app. + /// Looks up app details. /// - /// - Parameters: - /// - searchTerm: term for which to search in MAS. - /// - country: 2-letter ISO region code of the MAS in which to search. - /// - entity: OS platform of apps for which to search. - /// - Returns: URL for the search service or nil if searchTerm can't be encoded. - func searchURL( - for searchTerm: String, - inCountry country: String?, - ofEntity entity: Entity = .desktopSoftware - ) -> URL? { - url(.search, searchTerm, inCountry: country, ofEntity: entity) - } - - /// Builds the lookup URL for an app. + /// - Parameter appID: App ID. + /// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match, + /// or an `Error` if any problems occur. + func lookup(appID: AppID) -> Promise + /// Searches for apps. /// - /// - Parameters: - /// - appID: MAS app identifier. - /// - country: 2-letter ISO region code of the MAS in which to search. - /// - entity: OS platform of apps for which to search. - /// - Returns: URL for the lookup service or nil if appID can't be encoded. - func lookupURL( - forAppID appID: AppID, - inCountry country: String?, - ofEntity entity: Entity = .desktopSoftware - ) -> URL? { - url(.lookup, String(appID), inCountry: country, ofEntity: entity) - } - - private func url( - _ action: URLAction, - _ queryItemValue: String, - inCountry country: String?, - ofEntity entity: Entity = .desktopSoftware - ) -> URL? { - guard var components = URLComponents(string: "https://itunes.apple.com/\(action)") else { - return nil - } - - var queryItems = [ - URLQueryItem(name: "media", value: "software"), - URLQueryItem(name: "entity", value: entity.rawValue), - ] - - if let country { - queryItems.append(URLQueryItem(name: "country", value: country)) - } - - queryItems.append(URLQueryItem(name: action.queryItemName, value: queryItemValue)) - - components.queryItems = queryItems - - return components.url - } + /// - Parameter searchTerm: Term for which to search. + /// - Returns: A `Promise` of an `Array` of `SearchResult`s matching `searchTerm`. + func search(for searchTerm: String) -> Promise<[SearchResult]> } diff --git a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift index c6de32169..cc1939fac 100644 --- a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift +++ b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift @@ -32,39 +32,11 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher { self.networkManager = networkManager } - /// Searches for an app. - /// - /// - Parameter searchTerm: a search term matched against app names - /// - Returns: A Promise of an Array of SearchResults matching searchTerm - func search(for searchTerm: String) -> Promise<[SearchResult]> { - // Search for apps for compatible platforms, in order of preference. - // Macs with Apple Silicon can run iPad and iPhone apps. - var entities = [Entity.desktopSoftware] - if SysCtlSystemCommand.isAppleSilicon { - entities += [.iPadSoftware, .iPhoneSoftware] - } - - let results = entities.map { entity in - guard let url = searchURL(for: searchTerm, inCountry: country, ofEntity: entity) else { - fatalError("Failed to build URL for \(searchTerm)") - } - return loadSearchResults(url) - } - - // Combine the results, removing any duplicates. - var seenAppIDs = Set() - return when(fulfilled: results) - .flatMapValues { $0 } - .filterValues { result in - seenAppIDs.insert(result.trackId).inserted - } - } - /// Looks up app details. /// - /// - Parameter appID: MAS ID of app - /// - Returns: A Promise for 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. + /// - Parameter appID: App ID. + /// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match, + /// or an `Error` if any problems occur. func lookup(appID: AppID) -> Promise { guard let url = lookupURL(forAppID: appID, inCountry: country) else { fatalError("Failed to build URL for \(appID)") @@ -105,6 +77,34 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher { } } + /// Searches for apps from the MAS. + /// + /// - Parameter searchTerm: Term for which to search in the MAS. + /// - Returns: A `Promise` of an `Array` of `SearchResult`s matching `searchTerm`. + func search(for searchTerm: String) -> Promise<[SearchResult]> { + // Search for apps for compatible platforms, in order of preference. + // Macs with Apple Silicon can run iPad and iPhone apps. + var entities = [Entity.desktopSoftware] + if SysCtlSystemCommand.isAppleSilicon { + entities += [.iPadSoftware, .iPhoneSoftware] + } + + let results = entities.map { entity in + guard let url = searchURL(for: searchTerm, inCountry: country, ofEntity: entity) else { + fatalError("Failed to build URL for \(searchTerm)") + } + return loadSearchResults(url) + } + + // Combine the results, removing any duplicates. + var seenAppIDs = Set() + return when(fulfilled: results) + .flatMapValues { $0 } + .filterValues { result in + seenAppIDs.insert(result.trackId).inserted + } + } + private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> { firstly { networkManager.loadData(from: url) @@ -137,4 +137,81 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher { return version } } + + /// Builds the search URL for an app. + /// + /// - Parameters: + /// - searchTerm: term for which to search in MAS. + /// - country: 2-letter ISO region code of the MAS in which to search. + /// - entity: OS platform of apps for which to search. + /// - Returns: URL for the search service or nil if searchTerm can't be encoded. + func searchURL( + for searchTerm: String, + inCountry country: String?, + ofEntity entity: Entity = .desktopSoftware + ) -> URL? { + url(.search, searchTerm, inCountry: country, ofEntity: entity) + } + + /// Builds the lookup URL for an app. + /// + /// - Parameters: + /// - appID: App ID. + /// - country: 2-letter ISO region code of the MAS in which to search. + /// - entity: OS platform of apps for which to search. + /// - Returns: URL for the lookup service or nil if appID can't be encoded. + private func lookupURL( + forAppID appID: AppID, + inCountry country: String?, + ofEntity entity: Entity = .desktopSoftware + ) -> URL? { + url(.lookup, String(appID), inCountry: country, ofEntity: entity) + } + + private func url( + _ action: URLAction, + _ queryItemValue: String, + inCountry country: String?, + ofEntity entity: Entity = .desktopSoftware + ) -> URL? { + guard var components = URLComponents(string: "https://itunes.apple.com/\(action)") else { + return nil + } + + var queryItems = [ + URLQueryItem(name: "media", value: "software"), + URLQueryItem(name: "entity", value: entity.rawValue), + ] + + if let country { + queryItems.append(URLQueryItem(name: "country", value: country)) + } + + queryItems.append(URLQueryItem(name: action.queryItemName, value: queryItemValue)) + + components.queryItems = queryItems + + return components.url + } +} + +enum Entity: String { + case desktopSoftware + case macSoftware + case iPadSoftware + case iPhoneSoftware = "software" +} + +private enum URLAction { + case lookup + case search + + var queryItemName: String { + switch self { + case .lookup: + return "id" + case .search: + return "term" + } + } } diff --git a/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift index 3b72ee841..828ff31ab 100644 --- a/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift +++ b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherSpec.swift @@ -17,13 +17,13 @@ public class ITunesSearchAppStoreSearcherSpec: QuickSpec { MAS.initialize() } describe("url string") { - it("contains the app name") { + it("contains the search term") { expect { ITunesSearchAppStoreSearcher().searchURL(for: "myapp", inCountry: "US")?.absoluteString } == "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=myapp" } - it("contains the encoded app name") { + it("contains the encoded search term") { expect { ITunesSearchAppStoreSearcher().searchURL(for: "My App", inCountry: "US")?.absoluteString }