cloudsctl 0.5.0

CLI for managing your OpenStack configuration files.
Documentation
use anyhow::{Context, Result};
use indexmap::IndexMap;
use std::collections::HashMap;
use structopt::clap::arg_enum;
use term_table::{row::Row, table_cell::TableCell, Table};

type ListerResult = Result<fn(&str, Vec<&str>) -> Result<String>>;
type ShowOneResult = Result<fn(IndexMap<String, String>, Vec<String>) -> Result<String>>;

arg_enum! {
    #[derive(Debug, PartialEq)]
    #[allow(non_camel_case_types)]
    pub enum Format {
        table,
        value,
        yaml,
        json,
    }
}

fn value_lister(_: &str, values: Vec<&str>) -> Result<String> {
    Ok(values.iter().fold(String::new(), |acc, s| acc + s + "\n"))
}

fn yaml_lister(header: &str, values: Vec<&str>) -> Result<String> {
    let mut output: Vec<HashMap<&str, &str>> = vec![];
    for value in values.iter() {
        let mut temp: HashMap<&str, &str> = HashMap::new();
        let _ = temp.insert(header, value);
        output.push(temp);
    }
    serde_yaml::to_string(&output).context("Failed to output list as yaml.")
}

fn json_lister(header: &str, values: Vec<&str>) -> Result<String> {
    let mut output: Vec<HashMap<&str, &str>> = vec![];
    for value in values.iter() {
        let mut temp: HashMap<&str, &str> = HashMap::new();
        let _ = temp.insert(header, value);
        output.push(temp);
    }
    serde_json::to_string_pretty(&output).context("Failed to output list as json.")
}

fn table_lister(header: &str, values: Vec<&str>) -> Result<String> {
    let mut table = Table::new();
    table.style = term_table::TableStyle::simple();

    let header = Row::new(vec![TableCell::new(header)]);
    table.add_row(header);

    values.iter().for_each(|value| {
        let mut row = Row::new(vec![TableCell::new(value)]);
        row.has_separator = false;
        table.add_row(row);
    });

    if table.rows.len() > 1 {
        table.rows[1].has_separator = true;
    }

    Ok(table.render())
}

pub fn get_lister(format: Format) -> ListerResult {
    match format {
        Format::value => Ok(value_lister),
        Format::table => Ok(table_lister),
        Format::yaml => Ok(yaml_lister),
        Format::json => Ok(json_lister),
    }
}

fn value_showone(data: IndexMap<String, String>, retract_fields: Vec<String>) -> Result<String> {
    Ok(data
        .iter()
        .map(|(key, value)| {
            if retract_fields.contains(key) {
                "<redacted>"
            } else {
                value
            }
        })
        .fold(String::new(), |acc, value| acc + value + "\n"))
}

fn table_showone(data: IndexMap<String, String>, retract_fields: Vec<String>) -> Result<String> {
    let mut table = Table::new();
    table.style = term_table::TableStyle::simple();
    table.separate_rows = false;

    data.iter().for_each(|(key, value)| {
        let value = {
            if retract_fields.contains(key) {
                "<redacted>"
            } else {
                value.as_str()
            }
        };

        table.add_row(Row::new(vec![TableCell::new(key), TableCell::new(value)]));
    });

    Ok(table.render())
}

fn yaml_showone(data: IndexMap<String, String>, retract_fields: Vec<String>) -> Result<String> {
    let redacted = String::from("<redacted>");
    let mut temp: HashMap<_, _> = data.iter().collect();
    data.iter().for_each(|(key, value)| {
        let _ = temp.insert(key, {
            if retract_fields.contains(key) {
                &redacted
            } else {
                value
            }
        });
    });
    serde_yaml::to_string(&temp).context("Failed to output one result as yaml.")
}

fn json_showone(data: IndexMap<String, String>, retract_fields: Vec<String>) -> Result<String> {
    let redacted = String::from("<redacted>");
    let mut temp: HashMap<_, _> = data.iter().collect();
    data.iter().for_each(|(key, value)| {
        let _ = temp.insert(key, {
            if retract_fields.contains(key) {
                &redacted
            } else {
                value
            }
        });
    });
    serde_json::to_string_pretty(&temp).context("Failed to output one result as json.")
}

pub fn get_showone(format: Format) -> ShowOneResult {
    match format {
        Format::value => Ok(value_showone),
        Format::table => Ok(table_showone),
        Format::yaml => Ok(yaml_showone),
        Format::json => Ok(json_showone),
    }
}

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

    #[test]
    fn value_lister() {
        let values = vec!["foo", "bar"];
        assert_eq!(
            "foo\nbar\n",
            get_lister(Format::value).unwrap()("biz", values).unwrap()
        );
    }

    #[test]
    fn value_lister_empty() {
        let values = Vec::new();
        assert_eq!(
            "",
            get_lister(Format::value).unwrap()("biz", values).unwrap()
        );
    }

    #[test]
    fn table_lister() {
        let values = vec!["foo", "bar"];
        assert_eq!(
            "+-----+\n| biz |\n+-----+\n| foo |\n| bar |\n+-----+\n",
            get_lister(Format::table).unwrap()("biz", values).unwrap()
        );
    }

    #[test]
    fn table_lister_empty() {
        let values = vec![];
        assert_eq!(
            "+-----+\n| biz |\n+-----+\n",
            get_lister(Format::table).unwrap()("biz", values).unwrap()
        );
    }

    #[test]
    fn yaml_lister() {
        let values = vec!["foo", "bar"];
        assert_eq!(
            "---\n- biz: foo\n- biz: bar",
            get_lister(Format::yaml).unwrap()("biz", values).unwrap()
        );
    }

    #[test]
    fn yaml_lister_empty() {
        let values = Vec::new();
        assert_eq!(
            "---\n[]",
            get_lister(Format::yaml).unwrap()("biz", values).unwrap()
        );
    }

    #[test]
    fn json_lister() {
        let values = vec!["foo", "bar"];
        assert_eq!(
            "[\n  {\n    \"biz\": \"foo\"\n  },\n  {\n    \"biz\": \"bar\"\n  }\n]",
            get_lister(Format::json).unwrap()("biz", values).unwrap()
        );
    }

    #[test]
    fn json_lister_empty() {
        let values = Vec::new();
        assert_eq!(
            "[]",
            get_lister(Format::json).unwrap()("biz", values).unwrap()
        );
    }

    #[test]
    fn value_showone() {
        let mut data = IndexMap::new();
        let _ = data.insert("foo_key".into(), "foo_value".into());
        let _ = data.insert("bar_key".into(), "".into());

        assert_eq!(
            "foo_value\n\n",
            get_showone(Format::value).unwrap()(data, vec![]).unwrap()
        );
    }

    #[test]
    fn value_showone_empty() {
        let data = IndexMap::new();

        assert_eq!(
            "",
            get_showone(Format::value).unwrap()(data, vec![]).unwrap()
        );
    }

    #[test]
    fn value_showone_redacted() {
        let mut data = IndexMap::new();
        let _ = data.insert("foo_key".into(), "foo_value".into());
        let _ = data.insert("bar_key".into(), "".into());

        assert_eq!(
            "<redacted>\n\n",
            get_showone(Format::value).unwrap()(data, vec!["foo_key".into()]).unwrap()
        );
    }

    #[test]
    fn table_showone() {
        let mut map = indexmap::IndexMap::new();
        let _ = map.insert("foo".into(), "bar".into());

        assert_eq!(
            "+-----+-----+\n| foo | bar |\n+-----+-----+\n",
            get_showone(Format::table).unwrap()(map, vec![]).unwrap()
        );
    }

    #[test]
    fn table_showone_empty() {
        let map = indexmap::IndexMap::new();

        assert_eq!(
            "",
            get_showone(Format::table).unwrap()(map, vec![]).unwrap()
        );
    }

    #[test]
    fn table_showone_redacted() {
        let mut map = indexmap::IndexMap::new();
        let _ = map.insert("foo".into(), "bar".into());

        assert_eq!(
            "+-----+------------+\n| foo | <redacted> |\n+-----+------------+\n",
            get_showone(Format::table).unwrap()(map, vec!["foo".into()]).unwrap()
        );
    }

    #[test]
    fn yaml_showone() {
        let mut map = indexmap::IndexMap::new();
        let _ = map.insert("foo".into(), "bar".into());

        assert_eq!(
            "---\nfoo: bar",
            get_showone(Format::yaml).unwrap()(map, vec![]).unwrap()
        );
    }

    #[test]
    fn yaml_showone_empty() {
        let map = indexmap::IndexMap::new();

        assert_eq!(
            "---\n{}",
            get_showone(Format::yaml).unwrap()(map, vec![]).unwrap()
        );
    }

    #[test]
    fn yaml_showone_redacted() {
        let mut map = indexmap::IndexMap::new();
        let _ = map.insert("foo".into(), "bar".into());

        assert_eq!(
            "---\nfoo: \"<redacted>\"",
            get_showone(Format::yaml).unwrap()(map, vec!["foo".into()]).unwrap()
        );
    }

    #[test]
    fn json_showone() {
        let mut map = indexmap::IndexMap::new();
        let _ = map.insert("foo".into(), "bar".into());

        assert_eq!(
            "{\n  \"foo\": \"bar\"\n}",
            get_showone(Format::json).unwrap()(map, vec![]).unwrap()
        );
    }

    #[test]
    fn json_showone_empty() {
        let map = indexmap::IndexMap::new();

        assert_eq!(
            "{}",
            get_showone(Format::json).unwrap()(map, vec![]).unwrap()
        );
    }

    #[test]
    fn json_showone_redacted() {
        let mut map = indexmap::IndexMap::new();
        let _ = map.insert("foo".into(), "bar".into());

        assert_eq!(
            "{\n  \"foo\": \"<redacted>\"\n}",
            get_showone(Format::json).unwrap()(map, vec!["foo".into()]).unwrap()
        );
    }
}