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}",
}