mirror of
https://github.com/mas-cli/mas
synced 2025-02-16 12:38:30 +00:00
Refactor AppStoreSearcher
code.
Move code from `AppStoreSearcher` to `ITunesSearchAppStoreSearcher`. Improve DocC. Improve Quick test names. Resolve #607 Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com>
This commit is contained in:
parent
99eb91392e
commit
2a496b1f98
3 changed files with 119 additions and 113 deletions
|
@ -11,86 +11,15 @@ import PromiseKit
|
||||||
|
|
||||||
/// Protocol for searching the MAS catalog.
|
/// Protocol for searching the MAS catalog.
|
||||||
protocol AppStoreSearcher {
|
protocol AppStoreSearcher {
|
||||||
|
/// Looks up app details.
|
||||||
|
///
|
||||||
|
/// - 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<SearchResult?>
|
func lookup(appID: AppID) -> Promise<SearchResult?>
|
||||||
|
/// Searches for apps.
|
||||||
|
///
|
||||||
|
/// - 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]>
|
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.
|
|
||||||
///
|
|
||||||
/// - 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: 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -32,39 +32,11 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
||||||
self.networkManager = networkManager
|
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<AppID>()
|
|
||||||
return when(fulfilled: results)
|
|
||||||
.flatMapValues { $0 }
|
|
||||||
.filterValues { result in
|
|
||||||
seenAppIDs.insert(result.trackId).inserted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Looks up app details.
|
/// Looks up app details.
|
||||||
///
|
///
|
||||||
/// - Parameter appID: MAS ID of app
|
/// - Parameter appID: App ID.
|
||||||
/// - Returns: A Promise for the search result record of app, or nil if no apps match the ID,
|
/// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
|
||||||
/// or an Error if there is a problem with the network request.
|
/// or an `Error` if any problems occur.
|
||||||
func lookup(appID: AppID) -> Promise<SearchResult?> {
|
func lookup(appID: AppID) -> Promise<SearchResult?> {
|
||||||
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
|
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
|
||||||
fatalError("Failed to build URL for \(appID)")
|
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<AppID>()
|
||||||
|
return when(fulfilled: results)
|
||||||
|
.flatMapValues { $0 }
|
||||||
|
.filterValues { result in
|
||||||
|
seenAppIDs.insert(result.trackId).inserted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
|
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
|
||||||
firstly {
|
firstly {
|
||||||
networkManager.loadData(from: url)
|
networkManager.loadData(from: url)
|
||||||
|
@ -137,4 +137,81 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
||||||
return version
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,13 @@ public class ITunesSearchAppStoreSearcherSpec: QuickSpec {
|
||||||
MAS.initialize()
|
MAS.initialize()
|
||||||
}
|
}
|
||||||
describe("url string") {
|
describe("url string") {
|
||||||
it("contains the app name") {
|
it("contains the search term") {
|
||||||
expect {
|
expect {
|
||||||
ITunesSearchAppStoreSearcher().searchURL(for: "myapp", inCountry: "US")?.absoluteString
|
ITunesSearchAppStoreSearcher().searchURL(for: "myapp", inCountry: "US")?.absoluteString
|
||||||
}
|
}
|
||||||
== "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=myapp"
|
== "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 {
|
expect {
|
||||||
ITunesSearchAppStoreSearcher().searchURL(for: "My App", inCountry: "US")?.absoluteString
|
ITunesSearchAppStoreSearcher().searchURL(for: "My App", inCountry: "US")?.absoluteString
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue