Skip to content

Commit

Permalink
Refactor AppStoreSearcher code.
Browse files Browse the repository at this point in the history
Move code from `AppStoreSearcher` to `ITunesSearchAppStoreSearcher`.

Improve DocC.

Improve Quick test names.

Resolve #607

Signed-off-by: Ross Goldberg <[email protected]>
  • Loading branch information
rgoldberg committed Oct 27, 2024
1 parent 99eb913 commit 2a496b1
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 113 deletions.
89 changes: 9 additions & 80 deletions Sources/mas/Controllers/AppStoreSearcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,86 +11,15 @@ import PromiseKit

/// Protocol for searching the MAS catalog.
protocol AppStoreSearcher {
func lookup(appID: AppID) -> 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.
/// 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<SearchResult?>
/// 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]>
}
139 changes: 108 additions & 31 deletions Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppID>()
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<SearchResult?> {
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
fatalError("Failed to build URL for \(appID)")
Expand Down Expand Up @@ -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]> {
firstly {
networkManager.loadData(from: url)
Expand Down Expand Up @@ -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"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down

0 comments on commit 2a496b1

Please sign in to comment.