claps 0.2.0

CLIs written in Rust
Documentation
use std::path::{Path, PathBuf};
use std::time::Duration;

use anyhow::Result;
use clap::Args;

use reqwest::{Client, ClientBuilder, IntoUrl, Response};

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

#[derive(Debug, Args)]
pub(super) struct Cmd {
    #[command(flatten)]
    args: super::CommonArgs,
    #[arg(short, long, default_value = "https://gfw.liblaf.me")]
    api: String,
    #[arg(short, long, default_value = "/etc/sing-box/config.json")]
    config: PathBuf,
    #[arg(short, long)]
    dns: Option<String>,
    #[arg(short, long, default_value_t = false)]
    tun: bool,
}

#[async_trait::async_trait]
impl Run for Cmd {
    async fn run(self) -> Result<()> {
        let items = self.args.items()?;
        let client = Client::new();
        let request = client
            .get(format!("{}/convert/sing-box", self.api))
            .query(&[
                (
                    "sub",
                    items
                        .iter()
                        .filter_map(|item| item.notes())
                        .collect::<Vec<_>>()
                        .join("|"),
                ),
                ("dns", self.dns().await?),
                ("tun", self.tun.to_string()),
            ])
            .build()?;
        tracing::info!("{}", request.url());
        let response = client
            .execute(request)
            .await
            .log()?
            .error_for_status()
            .log()?;
        try_write(self.config, response.bytes().await.log()?)?;
        claps::external::sudo::sudo(["systemctl", "restart", "sing-box.service"])?;
        tracing::info!("sudo systemctl restart sing-box.service");
        Ok(())
    }
}

impl Cmd {
    async fn dns(&self) -> Result<String> {
        if let Some(dns) = self.dns.as_deref() {
            return Ok(dns.to_string());
        }
        match try_connect("https://101.6.6.6").await {
            Ok(_) => Ok("tuna".to_string()),
            Err(_) => Ok("alidns".to_string()),
        }
    }
}

fn try_write<P, C>(path: P, contents: C) -> Result<()>
where
    P: AsRef<Path>,
    C: AsRef<[u8]>,
{
    let path = path.as_ref();
    let contents = contents.as_ref();
    if let Ok(()) = std::fs::write(path, contents).log() {
        tracing::info!("write {:?}", path);
        return Ok(());
    }
    claps::external::sudo::tee(path, contents)?;
    tracing::info!("sudo tee {:?}", path);
    Ok(())
}

async fn try_connect<U>(url: U) -> Result<Response>
where
    U: IntoUrl,
{
    let client = ClientBuilder::new()
        .timeout(Duration::from_secs(1))
        .build()
        .log()?;
    let request = client.get(url).build().log()?;
    let response = client.execute(request).await.log()?;
    let response = response.error_for_status().log()?;
    Ok(response)
}