claps 0.2.0

CLIs written in Rust
Documentation
use std::fmt::Display;
use std::time::Duration;

use anyhow::Result;
use clap::Args;
use colored::Colorize;
use reqwest::{Client, ClientBuilder};
use serde::Deserialize;
use tabled::builder::Builder;
use tabled::settings::object::Columns;
use tabled::settings::Format;
use tabled::settings::{Modify, Style};

use claps::common::cmd::Run;
use claps::common::log::LogResult;

#[derive(Debug, Args)]
pub struct Cmd {
    #[arg(short, long, value_parser = parse_duration, default_value = "3")]
    timeout: Duration,
}

fn parse_duration(arg: &str) -> Result<Duration> {
    let seconds = arg.parse().log()?;
    Ok(Duration::from_secs(seconds))
}

async fn get_ip(client: &Client, url: &str) -> Result<String> {
    let request = client.get(url).build().log()?;
    Ok(client
        .execute(request)
        .await?
        .text()
        .await
        .log()?
        .trim()
        .to_string())
}

async fn get_geo_ip(client: &Client) -> Result<GeoIP> {
    let request = client.get("https://api.ip.sb/geoip").build().log()?;
    client.execute(request).await?.json().await.log()
}

#[derive(Debug, Deserialize)]
struct GeoIP {
    ip: String,
    country_code: String,
    country: String,
    region_code: Option<String>,
    region: Option<String>,
    city: Option<String>,
    postal_code: Option<String>,
    continent_code: String,
    latitude: f64,
    longitude: f64,
    organization: String,
    timezone: String,
}

impl Display for GeoIP {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        let mut builder = Builder::new();
        builder.push_record(["IP", &self.ip]);
        builder.push_record(["Country Code", &self.country_code]);
        builder.push_record(["Country", &self.country]);
        if let Some(region_code) = &self.region_code {
            builder.push_record(["Region Code", region_code]);
        }
        if let Some(region) = &self.region {
            builder.push_record(["Region", region]);
        }
        if let Some(city) = &self.city {
            builder.push_record(["City", city]);
        }
        if let Some(postal_code) = &self.postal_code {
            builder.push_record(["Postal Code", postal_code]);
        }
        builder.push_record(["Continent Code", &self.continent_code]);
        builder.push_record(["Latitude", &self.latitude.to_string()]);
        builder.push_record(["Longitude", &self.longitude.to_string()]);
        builder.push_record(["Organization", &self.organization]);
        builder.push_record(["Timezone", &self.timezone]);
        let mut table = builder.build();
        table
            .with(Style::blank())
            .with(
                Modify::new(Columns::first())
                    .with(Format::content(|s| s.bold().yellow().to_string())),
            )
            .with(
                Modify::new(Columns::single(1))
                    .with(Format::content(|s| s.bold().magenta().to_string())),
            );
        write!(f, "{}", table)
    }
}

#[async_trait::async_trait]
impl Run for Cmd {
    async fn run(self) -> Result<()> {
        let client = ClientBuilder::new().timeout(self.timeout).build().log()?;
        println!("{}", "-".repeat(57));
        for v in [4, 6] {
            match get_ip(&client, &format!("https://api-ipv{}.ip.sb/ip", v)).await {
                Ok(ip) => println!(
                    "{}: {}",
                    format!("IPv{}", v).bold().yellow(),
                    ip.to_string().bold().magenta()
                ),
                Err(_) => println!(
                    "{}: {}",
                    format!("IPv{}", v).bold().yellow(),
                    "-".bold().magenta()
                ),
            }
            println!("{}", "-".repeat(57));
        }
        match get_geo_ip(&client).await {
            Ok(geo_ip) => println!("{}", geo_ip),
            Err(_) => println!("{}: {}", "GeoIP".bold().yellow(), "-".bold().magenta()),
        }
        println!("{}", "-".repeat(57));
        Ok(())
    }
}