// 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")) """ } }