use crate::project::search_manifest;
use anyhow::{bail, ensure, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use toml_edit::{value, ArrayOfTables, Document, Item, Table};
const KEY_BIN: &str = "bin";
const KEY_BIN_NAME: &str = "name";
const KEY_BIN_PATH: &str = "path";
#[derive(Debug)]
pub struct Manifest {
root: Document,
path: PathBuf,
}
impl Manifest {
pub fn new() -> Result<Self> {
let path = search_manifest()?;
Self::open(&path)
}
fn open(path: &Path) -> Result<Self> {
let file_content = fs::read_to_string(path)
.with_context(|| format!("read toml file err, path: {:?}", path))?;
let mut doc = file_content
.parse::<Document>()
.with_context(|| format!("parse toml file err, path: {:?}", path))?;
let item = &doc[KEY_BIN];
match item {
Item::ArrayOfTables(_) => {
}
Item::None => {
doc[KEY_BIN] = Item::ArrayOfTables(ArrayOfTables::default());
}
_ => bail!("bin should be type ArrayOfTables instead of {:?}", item),
}
Ok(Self {
root: doc,
path: path.to_path_buf(),
})
}
fn bins(&self) -> &ArrayOfTables {
let item = &self.root[KEY_BIN];
match item {
Item::ArrayOfTables(v) => v,
_ => panic!("bin should be type ArrayOfTables instead of {:?}", item),
}
}
fn bins_mut(&mut self) -> &mut ArrayOfTables {
let item = &mut self.root[KEY_BIN];
match item {
Item::ArrayOfTables(v) => v,
_ => panic!("bin should be type ArrayOfTables instead of {:?}", item),
}
}
pub fn exists(&self, name: &str, path: &str) -> bool {
self.find_bin(name, path).is_some()
}
fn find_bin(&self, name: &str, path: &str) -> Option<usize> {
let bins = self.bins();
for i in 0..bins.len() {
if let Some(item) = bins.get(i) {
if let Some(v) = item[KEY_BIN_NAME].as_str() {
if v == name {
return Some(i);
}
}
if let Some(v) = item[KEY_BIN_PATH].as_str() {
if v == path {
return Some(i);
}
}
}
}
None
}
pub fn add_bin(&mut self, name: &str, path: &str) -> Result<()> {
ensure!(!name.is_empty(), "bin.name cannot be empty");
ensure!(!path.is_empty(), "bin.path cannot be empty");
self.remove_bin(name, path);
let mut table = Table::default();
table[KEY_BIN_NAME] = value(name);
table[KEY_BIN_PATH] = value(path);
self.bins_mut().append(table);
Ok(())
}
pub fn remove_bin(&mut self, name: &str, path: &str) -> bool {
match self.find_bin(name, path) {
Some(index) => {
self.bins_mut().remove(index);
true
}
None => false,
}
}
pub fn foreach_bin<F>(&self, mut f: F)
where
F: FnMut(Option<&str>, Option<&str>),
{
for item in self.bins().iter() {
let name = item[KEY_BIN_NAME].as_str();
let path = item[KEY_BIN_PATH].as_str();
f(name, path);
}
}
pub fn write(&self) -> Result<()> {
fs::write(&self.path, self.root.to_string_in_original_order())?;
Ok(())
}
}
impl ToString for Manifest {
fn to_string(&self) -> String {
self.root.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::project::*;
use anyhow::{Context, Result};
fn new_empty_manifest() -> Manifest {
let mut root = Document::new();
root[KEY_BIN] = Item::ArrayOfTables(ArrayOfTables::default());
Manifest {
root,
path: PathBuf::new(),
}
}
#[test]
fn open_manifest() -> Result<()> {
let dir = std::env::current_dir()
.context("get_current_dir error")?
.join("misc");
let file_path = search_manifest_from(&dir, "test-cargo.toml")?;
let manifest = Manifest::open(&file_path)?;
assert!(matches!(manifest.root[KEY_BIN], Item::ArrayOfTables(_)));
Ok(())
}
#[test]
fn add_bin() -> Result<()> {
let mut manifest = new_empty_manifest();
manifest.add_bin("bin1", "src/b1.rs")?;
manifest.add_bin("bin2", "src/b2.rs")?;
manifest.add_bin("bin3", "src/b3.rs")?;
manifest.add_bin("bin1", "src/2/b1.rs")?;
let expected = r#"[[bin]]
name = "bin2"
path = "src/b2.rs"
[[bin]]
name = "bin3"
path = "src/b3.rs"
[[bin]]
name = "bin1"
path = "src/2/b1.rs"
"#;
assert_eq!(expected, manifest.to_string());
Ok(())
}
#[test]
fn get_bins() -> Result<()> {
let mut manifest = new_empty_manifest();
assert_eq!(0, manifest.bins().len());
manifest.add_bin("bin1", "src/b1.rs")?;
assert_eq!(1, manifest.bins().len());
manifest.add_bin("bin2", "src/b2.rs")?;
assert_eq!(2, manifest.bins().len());
Ok(())
}
#[test]
fn bin_exists() -> Result<()> {
let mut manifest = new_empty_manifest();
assert!(!manifest.exists("bin1", "src/b1.rs"));
manifest.add_bin("bin1", "src/b1.rs")?;
assert!(manifest.exists("bin1", ""));
assert!(manifest.exists("", "src/b1.rs"));
Ok(())
}
#[test]
fn find_bin() {
let mut manifest = new_empty_manifest();
let index = manifest.find_bin("bin1", "src/b1.rs");
assert!(index.is_none());
manifest.add_bin("bin1", "src/b1.rs").unwrap();
assert_eq!(manifest.find_bin("bin1", "").unwrap(), 0);
assert_eq!(manifest.find_bin("", "src/b1.rs").unwrap(), 0);
}
#[test]
fn foreach_bin() -> Result<()> {
let mut manifest = new_empty_manifest();
manifest.add_bin("bin1", "src/b1.rs")?;
manifest.add_bin("bin2", "src/b2.rs")?;
manifest.add_bin("bin3", "src/b3.rs")?;
let mut bins = vec![];
manifest.foreach_bin(|name, path| {
let name = name.unwrap_or_default().to_string();
let path = path.unwrap_or_default().to_string();
bins.push((name, path));
});
assert_eq!(3, bins.len());
bins.sort();
assert_eq!(bins[0], ("bin1".to_string(), "src/b1.rs".to_string()));
assert_eq!(bins[1], ("bin2".to_string(), "src/b2.rs".to_string()));
assert_eq!(bins[2], ("bin3".to_string(), "src/b3.rs".to_string()));
Ok(())
}
}