querypie-cli 0.1.1

Query QueryPie databases from the terminal with webview authentication.
mod auth_cmd;
mod commands;
mod data_cmd;
mod session;
mod session_cmd;

use anyhow::Result;
use clap::Parser;
use std::fmt;

use self::commands::Command;
use crate::auth;
use crate::config;
use crate::formatting::style;
use crate::qpapi::GrpcError;

#[derive(Debug, Parser)]
#[command(name = "querypie")]
#[command(about = "Query QueryPie databases from the terminal")]
#[command(
    long_about = "QueryPie CLI authenticates with a lightweight webview session and runs catalog and SQL commands through QueryPie.",
    after_help = "EXAMPLES:
  querypie --host querypie.example.com auth login
  querypie --host querypie.example.com connection list
  querypie --host querypie.example.com -c 'example-main [US]' --engine mysql database list
  querypie --host querypie.example.com -c 'example-main [US]' --engine mysql query 'select 1;'"
)]
struct Cli {
    #[arg(long, global = true, value_name = "HOST", help = "QueryPie host")]
    host: Option<String>,
    #[arg(
        short = 'c',
        long,
        global = true,
        value_name = "CONNECTION",
        help = "QueryPie connection name"
    )]
    connection: Option<String>,
    #[arg(
        long,
        global = true,
        value_name = "ENGINE",
        help = "Database engine name, such as mysql"
    )]
    engine: Option<String>,
    #[arg(
        short = 'd',
        long = "db",
        global = true,
        value_name = "DATABASE",
        help = "Database name to use"
    )]
    database: Option<String>,
    #[arg(
        long,
        global = true,
        value_name = "SCHEMA",
        help = "Schema name to use for table commands"
    )]
    schema: Option<String>,
    #[arg(short, long, global = true, help = "Print verbose diagnostics")]
    verbose: bool,
    #[arg(
        long,
        global = true,
        value_name = "PATH",
        help = "Path to a QueryPie CLI config file"
    )]
    config: Option<String>,
    #[command(subcommand)]
    command: Command,
}

#[derive(Debug, Clone)]
pub(super) struct Global {
    host: String,
    connection: String,
    engine: String,
    database: String,
    schema: String,
    verbose: bool,
}

pub fn run() -> Result<()> {
    let cli = Cli::parse();
    let (global, command) = cli.into_global_and_command()?;
    command.run(&global)
}

impl Cli {
    fn into_global_and_command(self) -> Result<(Global, Command)> {
        let cfg = config::load(self.config.as_deref())?;
        let global = Global {
            host: pick(self.host, cfg.host),
            connection: pick(self.connection, cfg.connection),
            engine: self.engine.unwrap_or_default(),
            database: pick(self.database, cfg.database),
            schema: self.schema.unwrap_or_default(),
            verbose: self.verbose,
        };
        Ok((global, self.command))
    }
}

pub fn render_error(err: &anyhow::Error) {
    if let Some(err) = err.downcast_ref::<AuthLoginFailed>() {
        anstream::eprintln!("{} {}", style::error_icon(), err.message());
        return;
    }
    if auth::is_login_canceled(err) {
        anstream::eprintln!("{} {}", style::error_icon(), err);
        return;
    }
    if err.downcast_ref::<AuthStatusFailed>().is_some() {
        return;
    }
    if let Some(ge) = err.downcast_ref::<GrpcError>() {
        eprintln!("error: {}", ge.message);
        if let Some(hint) = ge.hint() {
            eprintln!("  {hint}");
        }
    } else {
        eprintln!("error: {err}");
    }
}

#[derive(Debug)]
pub(super) struct AuthLoginFailed {
    message: String,
}

impl AuthLoginFailed {
    fn message(&self) -> &str {
        &self.message
    }
}

impl From<anyhow::Error> for AuthLoginFailed {
    fn from(err: anyhow::Error) -> Self {
        Self {
            message: capitalize_first(&err.to_string()),
        }
    }
}

impl fmt::Display for AuthLoginFailed {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for AuthLoginFailed {}

#[derive(Debug)]
pub(super) struct AuthStatusFailed;

impl fmt::Display for AuthStatusFailed {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("auth status failed")
    }
}

impl std::error::Error for AuthStatusFailed {}

fn pick(flag: Option<String>, cfg: String) -> String {
    flag.filter(|s| !s.trim().is_empty()).unwrap_or(cfg)
}

fn capitalize_first(text: &str) -> String {
    let mut chars = text.chars();
    match chars.next() {
        Some(first) => first.to_uppercase().chain(chars).collect(),
        None => "Login failed".to_string(),
    }
}