cargo-ibuild 1.2.1

cargo-ibuild
use custom_error::custom_error;
use dirs::home_dir;
use regex::Regex;
use std::env;
use std::path::Path;
use std::{error::Error, fs, path::PathBuf};
use toml::value::Table;

#[derive(Debug)]
pub struct CargoToml {
    pub root: PathBuf,
    file: PathBuf,
    value: toml::Value,
    modified: bool,
}

impl CargoToml {
    pub fn new<T: AsRef<Path>>(root: Option<T>) -> Result<Self, Box<dyn Error>> {
        let mut root = root.map_or(env::current_dir()?, |f| f.as_ref().to_path_buf());
        let mut file: PathBuf;
        loop {
            file = root.join("Cargo.toml");
            if file.exists() {
                break;
            } else {
                root = match root.parent() {
                    Some(p) => p.to_path_buf(),
                    None => {
                        return Err(Box::new(TomlError::MissingToml));
                    }
                };
            }
        }

        let content = String::from_utf8(fs::read(&file)?)?;
        let value: toml::Value = if content.trim().is_empty() {
            toml::Value::Table(Table::new())
        } else {
            toml::from_str::<toml::Value>(content.as_str())?
        };

        Ok(CargoToml {
            root,
            file,
            value,
            modified: false,
        })
    }
    #[allow(dead_code)]
    pub fn from_cargo_config() -> Result<Self, Box<dyn Error>> {
        let root = home_dir().unwrap().join(".cargo");
        let value: toml::Value;
        let mut file = root.join("config");
        let mut exists = false;
        if file.exists() {
            exists = true;
        } else {
            file = root.join("config.toml");
            if file.exists() {
                exists = true;
            }
        }
        if exists {
            let content = String::from_utf8(fs::read(&file)?)?;
            value = if content.trim().is_empty() {
                toml::Value::Table(Table::new())
            } else {
                toml::from_str::<toml::Value>(content.as_str())?
            };
        } else {
            value = toml::Value::Table(Table::new());
        }
        Ok(CargoToml {
            root,
            file,
            value,
            modified: false,
        })
    }
    pub fn load(file: &PathBuf) -> Result<Self, Box<dyn Error>> {
        let content = String::from_utf8(fs::read(&file)?)?;
        let value: toml::Value = toml::from_str(content.as_str())?;
        Ok(CargoToml {
            root: file.parent().unwrap().to_path_buf(),
            file: file.clone(),
            value,
            modified: false,
        })
    }
    pub fn get<T: AsRef<str>>(
        &mut self,
        paths: &Vec<T>,
        default: Option<toml::Value>,
    ) -> Result<&mut toml::Value, Box<dyn Error>> {
        if paths.is_empty() {
            return Err(Box::new(TomlError::InvalidToml));
        }
        let mut node = &mut self.value;
        for (idx, path) in paths.iter().enumerate() {
            match node.as_table_mut() {
                Some(table) => {
                    if !table.contains_key(path.as_ref()) {
                        match default.clone() {
                            Some(d) => {
                                if idx == paths.len() - 1 {
                                    table.insert(String::from(path.as_ref()), d);
                                } else {
                                    table.insert(
                                        String::from(path.as_ref()),
                                        toml::Value::Table(Table::new()),
                                    );
                                }
                                self.modified = true;
                            }
                            None => {
                                return Err(Box::new(TomlError::InvalidTable {
                                    path: join_paths(paths),
                                }))
                            }
                        }
                    }
                    node = table.get_mut(path.as_ref()).unwrap();
                }
                None => {
                    return Err(Box::new(TomlError::InvalidTable {
                        path: join_paths(paths),
                    }))
                }
            }
        }
        Ok(node)
    }
    pub fn get_string<T: AsRef<str>>(
        &mut self,
        paths: &Vec<T>,
        default: Option<T>,
    ) -> Result<String, Box<dyn Error>> {
        let d = default.map(|d| toml::Value::String(String::from(d.as_ref())));
        let node = self.get(paths, d)?;
        if node.is_str() {
            Ok(String::from(node.as_str().unwrap()))
        } else {
            Err(Box::new(TomlError::InvalidString {
                path: join_paths(paths),
            }))
        }
    }
    pub fn get_string_array<T: AsRef<str>>(
        &mut self,
        paths: &Vec<T>,
        default: Option<Vec<T>>,
    ) -> Result<Vec<String>, Box<dyn Error>> {
        let d = default.map(|d| {
            toml::Value::Array(
                d.iter()
                    .map(|i| toml::Value::String(String::from(i.as_ref())))
                    .collect(),
            )
        });
        let node = self.get(paths, d)?;
        if node.is_array() {
            let mut arr = Vec::<String>::new();
            for i in node.as_array().unwrap() {
                if i.is_str() {
                    arr.push(String::from(i.as_str().unwrap()));
                } else {
                    return Err(Box::new(TomlError::InvalidStringArray {
                        path: join_paths(paths),
                    }));
                }
            }
            Ok(arr)
        } else {
            Err(Box::new(TomlError::InvalidStringArray {
                path: join_paths(paths),
            }))
        }
    }
    pub fn set_string_array<S: AsRef<str>, T: AsRef<str>>(
        &mut self,
        paths: &Vec<S>,
        value: Vec<T>,
    ) -> Result<(), Box<dyn Error>> {
        let default = Some(toml::Value::Array(Vec::new()));
        let node = self.get(paths, default)?;
        let arr = node.as_array_mut().unwrap();
        arr.clear();
        arr.extend(
            value
                .iter()
                .map(|i| toml::Value::String(String::from(i.as_ref()))),
        );
        self.modified = true;
        Ok(())
    }
    pub fn get_bool<T: AsRef<str>>(
        &mut self,
        paths: &Vec<T>,
        default: Option<bool>,
    ) -> Result<bool, Box<dyn Error>> {
        let d = default.map(|d| toml::Value::Boolean(d));
        let node = self.get(paths, d)?;
        if node.is_bool() {
            Ok(node.as_bool().unwrap())
        } else {
            Err(Box::new(TomlError::InvalidBoolean {
                path: join_paths(paths),
            }))
        }
    }
    pub fn get_integer<T: AsRef<str>>(
        &mut self,
        paths: &Vec<T>,
        default: Option<i64>,
    ) -> Result<i64, Box<dyn Error>> {
        let d = default.map(|d| toml::Value::Integer(d));
        let node = self.get(paths, d)?;
        if node.is_integer() {
            Ok(node.as_integer().unwrap())
        } else {
            Err(Box::new(TomlError::InvalidBoolean {
                path: join_paths(paths),
            }))
        }
    }
    pub fn to_string(&self) -> Result<String, Box<dyn Error>> {
        let mut val = self.value.clone();
        let cargo_features = {
            match val.as_table_mut().unwrap().remove("cargo-features") {
                Some(cargo_features) => {
                    let mut cargo_features_val = toml::Value::Table(Table::new());
                    cargo_features_val
                        .as_table_mut()
                        .unwrap()
                        .insert(String::from("cargo-features"), cargo_features);
                    toml::to_string_pretty(&cargo_features_val)?
                        .trim()
                        .to_string()
                }
                None => String::new(),
            }
        };
        let package = {
            match val.as_table_mut().unwrap().remove("package") {
                Some(package) => {
                    let mut root = toml::Value::Table(Table::new());
                    root.as_table_mut()
                        .unwrap()
                        .insert(String::from("package"), package);
                    toml::to_string_pretty(&root)?.trim().to_string()
                }
                None => String::new(),
            }
        };
        let profile = {
            match val.as_table_mut().unwrap().remove("profile") {
                Some(profile) => {
                    let mut root = toml::Value::Table(Table::new());
                    root.as_table_mut()
                        .unwrap()
                        .insert(String::from("profile"), profile);
                    toml::to_string_pretty(&root)?.trim().to_string()
                }
                None => String::new(),
            }
        };
        let body = toml::to_string_pretty(&val)?.trim().to_string();
        let mut content = format!(
            "{}\n\n{}\n\n{}\n\n{}\n",
            cargo_features, package, body, profile
        )
        .trim()
        .to_string();
        content = String::from(
            Regex::new(r"(\r?\n)+\[")
                .unwrap()
                .replace_all(content.as_str(), "$1$1[")
                .trim(),
        );
        Ok(content)
    }
    pub fn save(&mut self, force: bool) -> Result<(), Box<dyn Error>> {
        if self.modified || force {
            let content = self.to_string()?;
            fs::write(&self.file, content)?;
            self.modified = false;
        }
        Ok(())
    }
}

fn join_paths<T: AsRef<str>>(paths: &Vec<T>) -> String {
    paths
        .iter()
        .map(|item| item.as_ref())
        .collect::<Vec<&str>>()
        .join("/")
}

custom_error! {TomlError
    MissingToml = "Missing Toml",
    InvalidToml = "Invalid Toml",
    InvalidTable {path: String} = "Invalid Table: {path}",
    InvalidString {path: String} = "Invalid String: {path}",
    InvalidStringArray {path: String} = "Invalid String: {path}",
    InvalidBoolean {path: String} = "Invalid Boolean: {path}",
    InvalidInteger {path: String} = "Invalid Integer: {path}",
}