gistcafe 0.1.2

Useful gist.cafe utils
Documentation
use serde_json::*;
use std::*;
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use std::collections::BTreeMap;

pub fn vars<T>(args : &T)
where
    T: ?Sized + serde::ser::Serialize,
{
    if let Ok(inspect_vars_path) = env::var("INSPECT_VARS") {
        if let Ok(json) = to_string(&args) {
            let vars_path = inspect_vars_path.replace("\\","/");
            if vars_path.contains("/") {
                let dir = Path::new(&vars_path).parent().unwrap().to_str().unwrap();
                let _ = fs::create_dir_all(dir);
            }

            let _ = match File::create(&vars_path) {
                Ok(mut file) => file.write_all(json.as_bytes()),
                Err(err) => Err(err),
            };
        }
    }
}

pub fn dump<T>(args : &T) -> Option<String>
where
    T: ?Sized + serde::ser::Serialize,
{
    match to_string_pretty(&args) {
        Ok(json) => {
            Some(json.replace("\"",""))
        },
        _ => None
    }
}

pub fn print_dump<T>(args : &T)
where
    T: ?Sized + serde::ser::Serialize,
{
    match dump(args) {
        Some(s) => println!("{}", s),
        _ => (),
    }
}

pub fn to_list_map<T: Sized>(rows : &Vec<T>) -> Vec<Map<String,Value>>
where
    T: ?Sized + serde::ser::Serialize,
{
    let mut to: Vec<Map<String,Value>> = Vec::new();
    for row in rows {
        let map = from_str(&to_string(row).unwrap());
        to.push(map.unwrap());
    }
    return to;
}

pub fn all_keys(rows : &Vec<Map<String,Value>>) -> Vec<String>
{
    let mut to: Vec<String> = Vec::new();
    for row in rows {
        for key in row.keys() {
            if !to.contains(key) {
                to.push(key.to_string());
            }
        }
    }
    return to;
}

pub fn dump_table<T: Sized>(rows : &Vec<T>) -> Option<String>
where
    T: ?Sized + serde::ser::Serialize,
{
    return dump_table_columns(rows, Vec::new());
}

pub fn dump_table_columns<T: Sized>(rows: &Vec<T>, columns: Vec<&str>) -> Option<String>
where
    T: ?Sized + serde::ser::Serialize,
{
    if rows.is_empty() {
        return None;
    }
    let map_rows = to_list_map(rows);
    let keys = if columns.is_empty() {
        all_keys(&map_rows) 
    } else { 
        columns.iter().map(|x| x.to_string()).collect()
    };
    let mut col_sizes: BTreeMap<String,usize> = BTreeMap::new();

    for k in &keys {
        let mut max = k.len();
        for row in &map_rows {
            if let Some(col) = &row.get(k) {
                max = cmp::max(format!("{}", col).len(), max);
            }
        }
        col_sizes.insert(k.to_string(), max);
    }

    let col_sizes_length = col_sizes.len();
    let row_width: usize = col_sizes.values().sum::<usize>()
        + (col_sizes_length * 2)
        + (col_sizes_length + 1);

    let dashes = "-".repeat(row_width - 2);
    let mut sb: Vec<String> = Vec::new();
    sb.push(format!("+{}+", dashes));
    let mut head = "|".to_string();
    for k in &keys {
        head = format!("{}{}|", &head, &align_center(k, col_sizes[k], " "));
    }
    sb.push(head.to_string());
    sb.push(format!("|{}|", dashes));

    for row in &map_rows {
        let mut to = "|".to_string();
        for k in &keys {
            to = format!("{}{}!", &to, &align_auto(&row[k], col_sizes[k], " "));
        }
        sb.push(to);
    }
    sb.push(format!("+{}+", dashes));

    return Some(sb.join("\n"));
}

pub fn print_dump_table<T: Sized>(rows : &Vec<T>)
where
    T: ?Sized + serde::ser::Serialize,
{
    match dump_table(rows) {
        Some(s) => println!("{}", s),
        _ => (),
    }
}

pub fn print_dump_table_columns<T: Sized>(rows : &Vec<T>, columns: Vec<&str>)
where
    T: ?Sized + serde::ser::Serialize,
{
    match dump_table_columns(rows, columns) {
        Some(s) => println!("{}", s),
        _ => (),
    }
}

pub fn align_left(str: &str, len: usize, pad: &str) -> String {
    if len > 0 { 
        let a_len = len + 1 - str.len();
        if a_len > 0 {
            return format!("{}{}{}", pad, str, pad.repeat(a_len));
        }
    }
    return "".into();
}

pub fn align_center(str: &str, len: usize, pad: &str) -> String {
    if len > 0 {
        let n_len = str.len();
        let half = (len as f64 / 2.0 - n_len as f64 / 2.0).floor() as usize;
        let odds = ((n_len % 2) as i64 - (len % 2) as i64).abs() as usize;
        return format!("{}{}{}", pad.repeat(half + 1), str, pad.repeat(half + 1 + odds));
    }
    return "".into();
}

pub fn align_right(str: &str, len: usize, pad: &str) -> String {
    if len > 0 { 
        let a_len = len + 1 - str.len();
        if a_len > 0 {
            return format!("{}{}{}", pad.repeat(a_len), str, pad);
        }
    }
    return "".into();
}

pub fn align_auto(obj: &Value, len: usize, pad: &str) -> String {
    let str = match obj {
        Value::String(s) => format!("{}", s),
        _ => format!("{}", obj),
    };
    if str.len() <= len {
        return match obj {
            Value::Number(_) => align_right(&str, len, &pad),
            _ => align_left(&str, len, &pad),
        }
    }
    return str;
}


#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }

    fn args() -> Value {
        return json!([
            { "a": 1, "b":"foo" },
            { "a": 2.1, "b":"barbar" },
            { "a": 3.21, "b":"bazbazbaz" }
        ]);
    }

    #[test]
    fn inspect_vars() {
        vars(&args());
    }

    #[test]
    fn inspect_print_dump() {
        print_dump(&args());
    }

    #[test]
    fn inspect_print_dump_table() {
        print_dump_table(&args().as_array().unwrap());
    }

    #[tokio::test]
    async fn inspect_print_dump_table_json_api() {

        let org_name = "rust-lang";

        let res = reqwest::Client::new()
            .get(&format!("https://api.github.com/orgs/{}/repos", org_name))
            .header(reqwest::header::USER_AGENT, "gist.cafe")
            .send()
            .await.unwrap();

        let json: Vec<Map<String,Value>> = res.json().await.unwrap();
        let mut org_repos: Vec<Map<String,Value>> = Vec::new();
        for x in json.iter() {
            org_repos.push(json!({
                "name":        x["name"],
                "description": x["description"],
                "lang":        x["language"],
                "watchers":    x["watchers"],
                "forks":       x["forks"],
            }).as_object().unwrap().clone());
        }
        org_repos.sort_by(|a, b| b["watchers"].as_i64().cmp(&a["watchers"].as_i64()));
        
        println!("Top 3 {} GitHub Repos:", org_name);
        print_dump(&org_repos[1..=3]);

        println!("\nTop 10 {} GitHub Repos:", org_name);
        print_dump_table(&org_repos[1..=10].iter().map(|x| json!({
            "name":        x["name"],
            "lang":        x["lang"],
            "watchers":    x["watchers"],
            "forks":       x["forks"],
        }).as_object().unwrap().clone()).collect());

        println!("\nTop 10 {} GitHub Repos:", org_name);
        print_dump_table_columns(&org_repos[1..=10].to_vec(), 
            vec!["name", "lang", "watchers", "forks"]);
    }    
}