From 9f312b3df0b101d3beeba1b7309ae25e85d2fef8 Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Sat, 22 May 2021 18:20:52 +0200 Subject: [PATCH] Implement search command --- Source/Commands/Search.swift | 52 +++++++++++++ Source/IPATool.swift | 5 +- Source/iTunes/iTunesClient.swift | 40 ++++++++++ Source/iTunes/iTunesEndpoint.swift | 12 ++- Source/iTunes/iTunesRequest.swift | 10 ++- ipatool.xcodeproj/project.pbxproj | 4 + .../xcshareddata/xcschemes/ipatool.xcscheme | 78 +++++++++++++++++++ 7 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 Source/Commands/Search.swift create mode 100644 ipatool.xcodeproj/xcshareddata/xcschemes/ipatool.xcscheme diff --git a/Source/Commands/Search.swift b/Source/Commands/Search.swift new file mode 100644 index 0000000..d262aa9 --- /dev/null +++ b/Source/Commands/Search.swift @@ -0,0 +1,52 @@ +// +// Search.swift +// IPATool +// +// Created by Majd Alfhaily on 22.05.21. +// + +import ArgumentParser +import Foundation + +struct Search: ParsableCommand { + static var configuration: CommandConfiguration { + return .init(abstract: "Search for iOS apps available on the App Store.") + } + + @Option + private var logLevel: LogLevel = .info + + @Option + private var limit: Int = 5 + + @Argument(help: "The term to search for.") + var term: String +} + +extension Search { + func run() throws { + let logger = ConsoleLogger(level: logLevel) + + logger.log("Creating HTTP client...", level: .debug) + let httpClient = HTTPClient(urlSession: URLSession.shared) + + logger.log("Creating iTunes client...", level: .debug) + let itunesClient = iTunesClient(httpClient: httpClient) + + do { + logger.log("Searching for '\(term)'...", level: .info) + let results = try itunesClient.search(term: term, limit: limit) + + guard !results.isEmpty else { + logger.log("No results found.", level: .error) + _exit(1) + } + + logger.log("Found \(results.count) results:\n\(results.enumerated().map({ "\($0 + 1). \($1.name): \($1.bundleIdentifier) (\($1.version))." }).joined(separator: "\n"))", level: .info) + } catch { + logger.log("\(error)", level: .debug) + logger.log("An unknown error has occurred.", level: .error) + _exit(1) + } + } +} diff --git a/Source/IPATool.swift b/Source/IPATool.swift index 5fd45da..38b99e2 100644 --- a/Source/IPATool.swift +++ b/Source/IPATool.swift @@ -10,9 +10,8 @@ import ArgumentParser struct IPATool: ParsableCommand { static var configuration: CommandConfiguration { return .init(commandName: "ipatool", - abstract: "A cli tool for interacting with Apple ipa files.", + abstract: "A cli tool for interacting with Apple's ipa files.", version: "0.9.0", - subcommands: [Download.self], - defaultSubcommand: Download.self) + subcommands: [Download.self, Search.self]) } } diff --git a/Source/iTunes/iTunesClient.swift b/Source/iTunes/iTunesClient.swift index 0bd45dd..2c8e1e2 100644 --- a/Source/iTunes/iTunesClient.swift +++ b/Source/iTunes/iTunesClient.swift @@ -9,6 +9,7 @@ import Foundation protocol iTunesClientInterface { func lookup(bundleIdentifier: String, completion: @escaping (Result) -> Void) + func search(term: String, limit: Int, completion: @escaping (Result<[iTunesResponse.Result], Error>) -> Void) } extension iTunesClientInterface { @@ -32,6 +33,27 @@ extension iTunesClientInterface { return result } } + + func search(term: String, limit: Int) throws -> [iTunesResponse.Result] { + let semaphore = DispatchSemaphore(value: 0) + var result: Result<[iTunesResponse.Result], Error>? + + search(term: term, limit: limit) { + result = $0 + semaphore.signal() + } + + _ = semaphore.wait(timeout: .distantFuture) + + switch result { + case .none: + throw iTunesClient.Error.timeout + case let .failure(error): + throw error + case let .success(result): + return result + } + } } final class iTunesClient: iTunesClientInterface { @@ -59,6 +81,24 @@ final class iTunesClient: iTunesClientInterface { } } } + + func search(term: String, limit: Int, completion: @escaping (Result<[iTunesResponse.Result], Swift.Error>) -> Void) { + let request = iTunesRequest.search(term: term, limit: limit) + + httpClient.send(request) { result in + switch result { + case let .success(response): + do { + let decoded = try response.decode(iTunesResponse.self, as: .json) + completion(.success(decoded.results)) + } catch { + completion(.failure(error)) + } + case let .failure(error): + completion(.failure(error)) + } + } + } } extension iTunesClient { diff --git a/Source/iTunes/iTunesEndpoint.swift b/Source/iTunes/iTunesEndpoint.swift index f55e4a1..60c7937 100644 --- a/Source/iTunes/iTunesEndpoint.swift +++ b/Source/iTunes/iTunesEndpoint.swift @@ -8,14 +8,24 @@ import Foundation enum iTunesEndpoint { + case search case lookup } extension iTunesEndpoint: HTTPEndpoint { var url: URL { - var components = URLComponents(string: "/lookup")! + var components = URLComponents(string: path)! components.scheme = "https" components.host = "itunes.apple.com" return components.url! } + + private var path: String { + switch self { + case .search: + return "/search" + case .lookup: + return "/lookup" + } + } } diff --git a/Source/iTunes/iTunesRequest.swift b/Source/iTunes/iTunesRequest.swift index ab26b9e..8ae5c03 100644 --- a/Source/iTunes/iTunesRequest.swift +++ b/Source/iTunes/iTunesRequest.swift @@ -8,6 +8,7 @@ import Foundation enum iTunesRequest { + case search(term: String, limit: Int) case lookup(bundleIdentifier: String) } @@ -17,13 +18,20 @@ extension iTunesRequest: HTTPRequest { } var endpoint: HTTPEndpoint { - return iTunesEndpoint.lookup + switch self { + case .lookup: + return iTunesEndpoint.lookup + case .search: + return iTunesEndpoint.search + } } var payload: HTTPPayload? { switch self { case let .lookup(bundleIdentifier): return .urlEncoding(["media": "software", "bundleId": bundleIdentifier, "limit": "1"]) + case let .search(term, limit): + return .urlEncoding(["media": "software", "term": term, "limit": "\(limit)"]) } } } diff --git a/ipatool.xcodeproj/project.pbxproj b/ipatool.xcodeproj/project.pbxproj index 2bba769..8d92950 100644 --- a/ipatool.xcodeproj/project.pbxproj +++ b/ipatool.xcodeproj/project.pbxproj @@ -32,6 +32,7 @@ A9CA48FD26594B5F00BC09D5 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48FC26594B5F00BC09D5 /* Logging.swift */; }; A9CA48FF26594B7A00BC09D5 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48FE26594B7A00BC09D5 /* LogLevel.swift */; }; A9CA490126594BB000BC09D5 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA490026594BB000BC09D5 /* ConsoleLogger.swift */; }; + A9CA49032659627A00BC09D5 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA49022659627A00BC09D5 /* Search.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -71,6 +72,7 @@ A9CA48FC26594B5F00BC09D5 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; A9CA48FE26594B7A00BC09D5 /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = ""; }; A9CA490026594BB000BC09D5 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = ""; }; + A9CA49022659627A00BC09D5 /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -121,6 +123,7 @@ isa = PBXGroup; children = ( A9CA48CC2658FBF700BC09D5 /* Download.swift */, + A9CA49022659627A00BC09D5 /* Search.swift */, ); path = Commands; sourceTree = ""; @@ -266,6 +269,7 @@ A9CA48DC26591CF100BC09D5 /* HTTPPayload.swift in Sources */, A9CA48E72659273700BC09D5 /* iTunesEndpoint.swift in Sources */, A9CA48F22659317E00BC09D5 /* StoreResponse.swift in Sources */, + A9CA49032659627A00BC09D5 /* Search.swift in Sources */, A9CA48FA2659478600BC09D5 /* HTTPDownloadClient.swift in Sources */, A9CA48CD2658FBF700BC09D5 /* Download.swift in Sources */, ); diff --git a/ipatool.xcodeproj/xcshareddata/xcschemes/ipatool.xcscheme b/ipatool.xcodeproj/xcshareddata/xcschemes/ipatool.xcscheme new file mode 100644 index 0000000..6c04cd1 --- /dev/null +++ b/ipatool.xcodeproj/xcshareddata/xcschemes/ipatool.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +