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 {
|
||||
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])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import Foundation
|
|||
|
||||
protocol iTunesClientInterface {
|
||||
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 {
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = "<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>"; };
|
||||
A9CA49022659627A00BC09D5 /* Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
@ -121,6 +123,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
A9CA48CC2658FBF700BC09D5 /* Download.swift */,
|
||||
A9CA49022659627A00BC09D5 /* Search.swift */,
|
||||
);
|
||||
path = Commands;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 */,
|
||||
);
|
||||
|
|
|
@ -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