// Copyright © 2024 Ryan Booker. All rights reserved.
import ArgumentParser
import Domain
import Foundation
import Toolbox
public struct Search: AsyncParsableCommand {
public static let configuration = CommandConfiguration(
commandName: "search",
abstract: "Search for something (Beta, requires Kagi Business (Team) plan).",
usage: "kg search \"Do androids dream of electric sheep?\""
)
@Argument(help: "Something to search for")
public var query: String
@Option(help: "The maximum number of search results")
public var limit: Int?
var client = Client.liveValue
enum CodingKeys: CodingKey {
case query, limit
}
public init() {
// Intentionally left empty
}
public mutating func run() async throws {
do {
let response = try await Client.liveValue.run(query: query, limit: limit)
print(response)
} catch {
throw ValidationError(error.localizedDescription)
}
}
}
// MARK: - Processing
extension Search {
struct Client {
var run: @Sendable (
_ query: String,
_ limit: Int?
) async throws -> Response<[Either<SearchResult, RelatedResults>]>
@inlinable
func run(
query: String,
limit: Int?
) async throws -> Response<[Either<SearchResult, RelatedResults>]> {
try await run(query, limit)
}
}
}
extension Search.Client {
static let liveValue = Self(
run: { query, limit in
var queryItems: [URLQueryItem] = [.init(name: "q", value: query)]
if let limit {
queryItems.append(.init(name: "limit", value: "\(limit)"))
}
var request = try Endpoint.search.authorizedRequest
request.url?.append(queryItems: queryItems)
let (data, response) = try await URLSession.shared.data(for: request)
do {
return try JSONDecoder().decode(Response.self, from: data)
} catch {
#if DEBUG
print(response)
print(error)
print("-----")
#endif
throw error
}
}
)
}
// MARK: - Response
extension Search {
public struct SearchResult: CustomStringConvertible, Decodable {
public struct Thumbnail: Decodable {
public var url: URL
public var height: Int
public var width: Int
}
public var rank: Int
public var url: URL
public var title: String
public var snippet: String?
public var published: Date?
public var thumbnail: Thumbnail?
public var description: String {
[
"""
\(rank): \(title)
\(url)
-----
""",
snippet.map {
"""
\($0)
-----
"""
},
published.map {
"""
\($0.formatted(date: .long, time: .shortened))
-----
"""
}
]
.compactMap { $0 }
.joined(separator: "\n")
}
}
public struct RelatedResults: CustomStringConvertible, Decodable {
public var list: [String]
public var description: String {
"""
Possibly related searches:
\(list.map { "- \($0)" })
"""
}
}
}
extension Array where Element == Either<Search.SearchResult, Search.RelatedResults> {
public var description: String {
"""
Results:
\(map(\.description).joined(separator: "\n\n"))
"""
}
}