querypie-cli 0.1.1

Query QueryPie databases from the terminal with webview authentication.
pub(crate) mod style;
mod table;

use anyhow::Result;
use clap::ValueEnum;
use serde::Serialize;

use crate::auth::{AuthCheck, AuthState};
use crate::qpapi::{Connection, ResultSet, TableStructure};
use crate::sessioncache;

use self::table::print_table;

#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum OutputFormat {
    Text,
    Json,
}

#[derive(Debug, Clone, Copy)]
pub struct Options {
    pub output: OutputFormat,
    pub truncate: bool,
}

pub fn names(names: &[String], opts: Options) -> Result<()> {
    if opts.output == OutputFormat::Json {
        print_json(names)?;
    } else {
        for name in names {
            println!("{name}");
        }
    }
    Ok(())
}

pub fn connections(conns: &[Connection], opts: Options) -> Result<()> {
    #[derive(Serialize)]
    struct Row<'a> {
        name: &'a str,
        engine: &'a str,
        access: &'a str,
        deactivated: bool,
        cluster_uuid: &'a str,
        db_type: i32,
    }

    let rows = conns
        .iter()
        .map(|c| Row {
            name: &c.name,
            engine: c.engine(),
            access: connection_access(c),
            deactivated: c.deactivated,
            cluster_uuid: &c.cluster_uuid,
            db_type: c.db_type,
        })
        .collect::<Vec<_>>();

    if opts.output == OutputFormat::Json {
        print_json(&rows)?;
    } else {
        print_table(
            &["NAME", "ENGINE", "ACCESS"],
            rows.iter().map(|row| {
                vec![
                    row.name.to_string(),
                    row.engine.to_string(),
                    row.access.to_string(),
                ]
            }),
            opts.truncate,
        );
    }
    Ok(())
}

pub fn sessions(entries: &[sessioncache::Entry], opts: Options) -> Result<()> {
    if opts.output == OutputFormat::Json {
        #[derive(Serialize)]
        struct Row<'a> {
            host: &'a str,
            connection: &'a str,
            engine: &'a str,
            db: &'a str,
            session: &'a str,
            opened_at: i64,
        }
        let rows = entries
            .iter()
            .map(|entry| Row {
                host: &entry.host,
                connection: &entry.connection,
                engine: &entry.engine,
                db: &entry.db,
                session: &entry.session,
                opened_at: entry.opened_at,
            })
            .collect::<Vec<_>>();
        print_json(&rows)?;
    } else {
        print_table(
            &["HOST", "CONNECTION", "ENGINE", "DB", "SESSION"],
            entries.iter().map(|entry| {
                vec![
                    entry.host.clone(),
                    entry.connection.clone(),
                    entry.engine.clone(),
                    entry.db.clone(),
                    entry.session.clone(),
                ]
            }),
            opts.truncate,
        );
    }
    Ok(())
}

pub fn auth_status(checks: &[AuthCheck]) -> Result<bool> {
    let mut failed = false;
    for (index, check) in checks.iter().enumerate() {
        if index > 0 {
            println!();
        }
        println!("{}", check.host);
        match check.state {
            AuthState::Missing => {
                failed = true;
                anstream::println!(
                    "  {} Failed to log in to {}",
                    style::error_icon(),
                    check.host
                );
                println!("  - No QueryPie webview session was found.");
                println!(
                    "  - To authenticate, run: querypie --host {} auth login",
                    check.host
                );
            }
            AuthState::Valid => {
                anstream::println!("  {} Logged in to {}", style::success_icon(), check.host);
                println!("  - Webview session: active");
                println!("  - Cookie store: QueryPie webview profile");
            }
            AuthState::Expired => {
                failed = true;
                anstream::println!(
                    "  {} Failed to log in to {}",
                    style::error_icon(),
                    check.host
                );
                println!("  - The QueryPie webview session is expired.");
                println!(
                    "  - To re-authenticate, run: querypie --host {} auth login",
                    check.host
                );
            }
        }
    }
    Ok(failed)
}

pub fn script(script: &str, opts: Options) -> Result<()> {
    if opts.output == OutputFormat::Json {
        print_json(&serde_json::json!({ "script": script }))?;
    } else {
        println!("{script}");
    }
    Ok(())
}

pub fn table_structure(structure: &TableStructure, opts: Options) -> Result<()> {
    if opts.output == OutputFormat::Json {
        print_json(structure)?;
        return Ok(());
    }

    print_table(
        &structure
            .headers
            .iter()
            .map(String::as_str)
            .collect::<Vec<_>>(),
        structure.rows.iter().cloned(),
        opts.truncate,
    );
    Ok(())
}

pub fn query_result(res: &ResultSet, opts: Options) -> Result<()> {
    if opts.output == OutputFormat::Json {
        print_json(&serde_json::json!({ "columns": &res.columns, "rows": query_rows_json(res) }))?;
        return Ok(());
    }

    print_table(
        &res.columns
            .iter()
            .map(|c| c.name.as_str())
            .collect::<Vec<_>>(),
        res.rows.iter().map(|row| {
            row.iter()
                .map(|c| {
                    if c.is_null {
                        style::null_value()
                    } else {
                        c.value.clone()
                    }
                })
                .collect::<Vec<_>>()
        }),
        opts.truncate,
    );
    println!("\n({} rows)", res.rows.len());
    Ok(())
}

fn connection_access(conn: &Connection) -> &'static str {
    if conn.deactivated {
        "expired"
    } else {
        "active"
    }
}

fn print_json<T: Serialize + ?Sized>(value: &T) -> Result<()> {
    println!("{}", serde_json::to_string_pretty(value)?);
    Ok(())
}

fn query_rows_json(res: &ResultSet) -> Vec<Vec<serde_json::Value>> {
    res.rows
        .iter()
        .map(|row| {
            row.iter()
                .map(|cell| {
                    if cell.is_null {
                        serde_json::Value::Null
                    } else {
                        serde_json::Value::String(cell.value.clone())
                    }
                })
                .collect()
        })
        .collect()
}