use std::io::{self, BufRead};
use base64::encode;
use clap::Parser;
use console::style;
use reqwest::ClientBuilder;
use serde::{Deserialize, Serialize};
use serde_json::{self, json};
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
#[clap(short, long)]
verbose: bool,
#[clap(short, long)]
json: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FoundResponse {
pub packager: String,
pub packages: Vec<Package>,
pub probability: f64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Package {
pub package: String,
pub name: String,
pub url: String,
pub homepage: Option<String>,
pub version: Option<String>,
pub summary: Option<String>,
pub updated_at: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ErrorResponse {
pub error: String,
pub packager: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum Response {
Found(FoundResponse),
Error(ErrorResponse),
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
let input = read_all_lines_from_stdin().unwrap();
let client = ClientBuilder::new()
.user_agent("Alpakr CLI (rust)")
.build()
.unwrap();
let encoded = encode(&input.trim().to_string());
let payload = json!({ "fragment": encoded });
if args.verbose {
eprintln!("payload = {}", payload);
}
let response = client
.post("https://api.alpakr.code.boutique/v1/lookup")
.header("Content-Type", "application/json")
.body(payload.to_string())
.send()
.await?;
let text = response.text().await.unwrap();
if args.verbose {
eprintln!("response = {:?}", text);
}
let maybe_response: Result<Response, _> = serde_json::from_str(&text);
match maybe_response {
Ok(response) => match response {
Response::Found(found_response) => {
if args.json {
if found_response.packages.is_empty() {
println!("{}", json!({"error": "No packages found"}));
} else {
println!("{}", json!(found_response));
}
} else {
let packager = found_response.packager.as_str();
let p = match packager {
"cargo" => style(format!(" {} ", packager)).bold().black().on_green(),
"cargo-compiling" => {
style(format!(" {} ", "cargo")).bold().black().on_green()
}
"composer" => style(format!(" {} ", packager)).bold().black().on_yellow(),
"gem" => style(format!(" {} ", packager)).bold().white().on_red(),
"npm" => style(format!(" {} ", packager)).bold().white().on_red(),
"pip" => style(format!(" {} ", packager)).bold().white().on_blue(),
_ => style(format!(" {} ", packager)).bold().white(),
};
println!("{}", p);
println!("");
if found_response.packages.is_empty() {
println!("{}", style("No packages found").bold().yellow());
} else {
let packages = found_response.packages;
let num_packages = packages.len();
for (i, package) in packages.into_iter().enumerate() {
println!(
"{} -> {}",
style(package.name).green(),
style(
package
.summary
.unwrap_or("no description available".to_string())
)
);
println!("{}", style(package.url).dim());
if (i + 1) < num_packages {
println!("");
}
}
}
}
}
Response::Error(error_response) => {
if args.json {
println!("{}", json!({ "error": error_response.error }));
} else {
eprintln!("error = {}", style(error_response.error).bold().red());
eprintln!("response = {}", style(text).bold().red());
}
}
},
Err(err) => {
eprintln!("{}", style(format!("{:?}", err)).bold().red());
eprintln!("{}", style("Use --verbose for more details").bold().red());
}
}
Ok(())
}
fn read_all_lines_from_stdin() -> io::Result<String> {
let stdin = io::stdin();
let mut lines = String::new();
for line in stdin.lock().lines() {
lines.push_str(&line?);
lines.push_str("\n");
}
Ok(lines.trim().trim().to_owned())
}