Implement search command

This commit is contained in:
Majd Alfhaily 2021-05-22 18:20:52 +02:00
parent 4f7fabfd18
commit 9f312b3df0
7 changed files with 196 additions and 5 deletions

View File

@ -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)
}
}
}

View File

@ -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])
}
}

View File

@ -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 {

View File

@ -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"
}
}
}

View File

@ -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)"])
}
}
}

View File

@ -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 */,
);

View File

@ -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>