Implement search command
This commit is contained in:
parent
4f7fabfd18
commit
9f312b3df0
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,9 +10,8 @@ import ArgumentParser
|
||||||
struct IPATool: ParsableCommand {
|
struct IPATool: ParsableCommand {
|
||||||
static var configuration: CommandConfiguration {
|
static var configuration: CommandConfiguration {
|
||||||
return .init(commandName: "ipatool",
|
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",
|
version: "0.9.0",
|
||||||
subcommands: [Download.self],
|
subcommands: [Download.self, Search.self])
|
||||||
defaultSubcommand: Download.self)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import Foundation
|
||||||
|
|
||||||
protocol iTunesClientInterface {
|
protocol iTunesClientInterface {
|
||||||
func lookup(bundleIdentifier: String, completion: @escaping (Result<iTunesResponse.Result, Error>) -> Void)
|
func lookup(bundleIdentifier: String, completion: @escaping (Result<iTunesResponse.Result, Error>) -> Void)
|
||||||
|
func search(term: String, limit: Int, completion: @escaping (Result<[iTunesResponse.Result], Error>) -> Void)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension iTunesClientInterface {
|
extension iTunesClientInterface {
|
||||||
|
@ -32,6 +33,27 @@ extension iTunesClientInterface {
|
||||||
return result
|
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 {
|
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 {
|
extension iTunesClient {
|
||||||
|
|
|
@ -8,14 +8,24 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum iTunesEndpoint {
|
enum iTunesEndpoint {
|
||||||
|
case search
|
||||||
case lookup
|
case lookup
|
||||||
}
|
}
|
||||||
|
|
||||||
extension iTunesEndpoint: HTTPEndpoint {
|
extension iTunesEndpoint: HTTPEndpoint {
|
||||||
var url: URL {
|
var url: URL {
|
||||||
var components = URLComponents(string: "/lookup")!
|
var components = URLComponents(string: path)!
|
||||||
components.scheme = "https"
|
components.scheme = "https"
|
||||||
components.host = "itunes.apple.com"
|
components.host = "itunes.apple.com"
|
||||||
return components.url!
|
return components.url!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var path: String {
|
||||||
|
switch self {
|
||||||
|
case .search:
|
||||||
|
return "/search"
|
||||||
|
case .lookup:
|
||||||
|
return "/lookup"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum iTunesRequest {
|
enum iTunesRequest {
|
||||||
|
case search(term: String, limit: Int)
|
||||||
case lookup(bundleIdentifier: String)
|
case lookup(bundleIdentifier: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,13 +18,20 @@ extension iTunesRequest: HTTPRequest {
|
||||||
}
|
}
|
||||||
|
|
||||||
var endpoint: HTTPEndpoint {
|
var endpoint: HTTPEndpoint {
|
||||||
return iTunesEndpoint.lookup
|
switch self {
|
||||||
|
case .lookup:
|
||||||
|
return iTunesEndpoint.lookup
|
||||||
|
case .search:
|
||||||
|
return iTunesEndpoint.search
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var payload: HTTPPayload? {
|
var payload: HTTPPayload? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .lookup(bundleIdentifier):
|
case let .lookup(bundleIdentifier):
|
||||||
return .urlEncoding(["media": "software", "bundleId": bundleIdentifier, "limit": "1"])
|
return .urlEncoding(["media": "software", "bundleId": bundleIdentifier, "limit": "1"])
|
||||||
|
case let .search(term, limit):
|
||||||
|
return .urlEncoding(["media": "software", "term": term, "limit": "\(limit)"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
A9CA48FD26594B5F00BC09D5 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48FC26594B5F00BC09D5 /* Logging.swift */; };
|
A9CA48FD26594B5F00BC09D5 /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48FC26594B5F00BC09D5 /* Logging.swift */; };
|
||||||
A9CA48FF26594B7A00BC09D5 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48FE26594B7A00BC09D5 /* LogLevel.swift */; };
|
A9CA48FF26594B7A00BC09D5 /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA48FE26594B7A00BC09D5 /* LogLevel.swift */; };
|
||||||
A9CA490126594BB000BC09D5 /* ConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CA490026594BB000BC09D5 /* ConsoleLogger.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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
@ -71,6 +72,7 @@
|
||||||
A9CA48FC26594B5F00BC09D5 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
|
A9CA48FC26594B5F00BC09D5 /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
|
||||||
A9CA48FE26594B7A00BC09D5 /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = "<group>"; };
|
A9CA48FE26594B7A00BC09D5 /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = "<group>"; };
|
||||||
A9CA490026594BB000BC09D5 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
|
A9CA490026594BB000BC09D5 /* ConsoleLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleLogger.swift; sourceTree = "<group>"; };
|
||||||
|
A9CA49022659627A00BC09D5 /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -121,6 +123,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A9CA48CC2658FBF700BC09D5 /* Download.swift */,
|
A9CA48CC2658FBF700BC09D5 /* Download.swift */,
|
||||||
|
A9CA49022659627A00BC09D5 /* Search.swift */,
|
||||||
);
|
);
|
||||||
path = Commands;
|
path = Commands;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -266,6 +269,7 @@
|
||||||
A9CA48DC26591CF100BC09D5 /* HTTPPayload.swift in Sources */,
|
A9CA48DC26591CF100BC09D5 /* HTTPPayload.swift in Sources */,
|
||||||
A9CA48E72659273700BC09D5 /* iTunesEndpoint.swift in Sources */,
|
A9CA48E72659273700BC09D5 /* iTunesEndpoint.swift in Sources */,
|
||||||
A9CA48F22659317E00BC09D5 /* StoreResponse.swift in Sources */,
|
A9CA48F22659317E00BC09D5 /* StoreResponse.swift in Sources */,
|
||||||
|
A9CA49032659627A00BC09D5 /* Search.swift in Sources */,
|
||||||
A9CA48FA2659478600BC09D5 /* HTTPDownloadClient.swift in Sources */,
|
A9CA48FA2659478600BC09D5 /* HTTPDownloadClient.swift in Sources */,
|
||||||
A9CA48CD2658FBF700BC09D5 /* Download.swift in Sources */,
|
A9CA48CD2658FBF700BC09D5 /* Download.swift in Sources */,
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1250"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A9CA48BD2658F93F00BC09D5"
|
||||||
|
BuildableName = "IPATool"
|
||||||
|
BlueprintName = "IPATool"
|
||||||
|
ReferencedContainer = "container:ipatool.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A9CA48BD2658F93F00BC09D5"
|
||||||
|
BuildableName = "IPATool"
|
||||||
|
BlueprintName = "IPATool"
|
||||||
|
ReferencedContainer = "container:ipatool.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "A9CA48BD2658F93F00BC09D5"
|
||||||
|
BuildableName = "IPATool"
|
||||||
|
BlueprintName = "IPATool"
|
||||||
|
ReferencedContainer = "container:ipatool.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
Loading…
Reference in New Issue