alpakr-cli 0.2.3

Alpakr CLI 🦙 a package lookup tool for: crates, npm, pypi, and more.
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 {
    /// Show additional output
    #[clap(short, long)]
    verbose: bool,

    /// Output as json
    #[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())
}