use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::{env, str};
use termcolor::{BufferWriter, Color, ColorChoice, ColorSpec, WriteColor};
use toml_edit;
use crate::dependency::Dependency;
use crate::errors::*;
const MANIFEST_FILENAME: &str = "Cargo.toml";
#[derive(Debug, Clone)]
pub struct Manifest {
pub data: toml_edit::Document,
}
pub fn find(specified: &Option<PathBuf>) -> Result<PathBuf> {
match *specified {
Some(ref path)
if fs::metadata(&path)
.chain_err(|| "Failed to get cargo file metadata")?
.is_file() =>
{
Ok(path.to_owned())
}
Some(ref path) => search(path),
None => search(&env::current_dir().chain_err(|| "Failed to get current directory")?),
}
}
fn search(dir: &Path) -> Result<PathBuf> {
let manifest = dir.join(MANIFEST_FILENAME);
if fs::metadata(&manifest).is_ok() {
Ok(manifest)
} else {
dir.parent()
.ok_or_else(|| ErrorKind::MissingManifest.into())
.and_then(|dir| search(dir))
}
}
fn merge_inline_table(old_dep: &mut toml_edit::Item, new: &toml_edit::Item) {
for (k, v) in new
.as_inline_table()
.expect("expected an inline table")
.iter()
{
old_dep[k] = toml_edit::value(v.clone());
}
}
fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
}
fn merge_dependencies(old_dep: &mut toml_edit::Item, new: &Dependency) {
assert!(!old_dep.is_none());
let new_toml = new.to_toml().1;
if str_or_1_len_table(old_dep) {
*old_dep = new_toml;
} else if old_dep.is_table_like() {
for key in &["version", "path", "git"] {
old_dep[key] = toml_edit::Item::None;
}
if let Some(name) = new_toml.as_str() {
old_dep["version"] = toml_edit::value(name);
} else {
merge_inline_table(old_dep, &new_toml);
}
} else {
unreachable!("Invalid old dependency type");
}
if let Some(t) = old_dep.as_inline_table_mut() {
t.fmt()
}
}
fn print_upgrade_if_necessary(
crate_name: &str,
old_dep: &toml_edit::Item,
new_version: &toml_edit::Item,
) -> Result<()> {
let old_version = if str_or_1_len_table(old_dep) {
old_dep.clone()
} else if old_dep.is_table_like() {
let version = old_dep["version"].clone();
if version.is_none() {
return Err("Missing version field".into());
}
version
} else {
unreachable!("Invalid old dependency type")
};
if let (Some(old_version), Some(new_version)) = (old_version.as_str(), new_version.as_str()) {
if old_version == new_version {
return Ok(());
}
let bufwtr = BufferWriter::stdout(ColorChoice::Always);
let mut buffer = bufwtr.buffer();
buffer
.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))
.chain_err(|| "Failed to set output colour")?;
write!(&mut buffer, " Upgrading ").chain_err(|| "Failed to write upgrade message")?;
buffer
.set_color(&ColorSpec::new())
.chain_err(|| "Failed to clear output colour")?;
writeln!(
&mut buffer,
"{} v{} -> v{}",
crate_name, old_version, new_version,
)
.chain_err(|| "Failed to write upgrade versions")?;
bufwtr
.print(&buffer)
.chain_err(|| "Failed to print upgrade message")?;
}
Ok(())
}
impl Manifest {
pub fn find_file(path: &Option<PathBuf>) -> Result<File> {
find(path).and_then(|path| {
OpenOptions::new()
.read(true)
.write(true)
.open(path)
.chain_err(|| "Failed to find Cargo.toml")
})
}
pub fn open(path: &Option<PathBuf>) -> Result<Manifest> {
let mut file = Manifest::find_file(path)?;
let mut data = String::new();
file.read_to_string(&mut data)
.chain_err(|| "Failed to read manifest contents")?;
data.parse().chain_err(|| "Unable to parse Cargo.toml")
}
pub fn get_table<'a>(&'a mut self, table_path: &[String]) -> Result<&'a mut toml_edit::Item> {
fn descend<'a>(
input: &'a mut toml_edit::Item,
path: &[String],
) -> Result<&'a mut toml_edit::Item> {
if let Some(segment) = path.get(0) {
let value = input[&segment].or_insert(toml_edit::table());
if value.is_table_like() {
descend(value, &path[1..])
} else {
Err(ErrorKind::NonExistentTable(segment.clone()).into())
}
} else {
Ok(input)
}
}
descend(&mut self.data.root, table_path)
}
pub fn get_sections(&self) -> Vec<(Vec<String>, toml_edit::Item)> {
let mut sections = Vec::new();
for dependency_type in &["dev-dependencies", "build-dependencies", "dependencies"] {
if self.data[dependency_type].is_table_like() {
sections.push((
vec![dependency_type.to_string()],
self.data[dependency_type].clone(),
))
}
let target_sections = self
.data
.as_table()
.get("target")
.and_then(toml_edit::Item::as_table_like)
.into_iter()
.flat_map(toml_edit::TableLike::iter)
.filter_map(|(target_name, target_table)| {
let dependency_table = &target_table[dependency_type];
dependency_table.as_table_like().map(|_| {
(
vec![
"target".to_string(),
target_name.to_string(),
dependency_type.to_string(),
],
dependency_table.clone(),
)
})
});
sections.extend(target_sections);
}
sections
}
pub fn write_to_file(&self, file: &mut File) -> Result<()> {
if self.data["package"].is_none() && self.data["project"].is_none() {
if !self.data["workspace"].is_none() {
Err(ErrorKind::UnexpectedRootManifest)?;
} else {
Err(ErrorKind::InvalidManifest)?;
}
}
let s = self.data.to_string();
let new_contents_bytes = s.as_bytes();
file.set_len(new_contents_bytes.len() as u64)
.chain_err(|| "Failed to truncate Cargo.toml")?;
file.write_all(new_contents_bytes)
.chain_err(|| "Failed to write updated Cargo.toml")
}
pub fn insert_into_table(&mut self, table_path: &[String], dep: &Dependency) -> Result<()> {
let table = self.get_table(table_path)?;
if table[&dep.name].is_none() {
let (ref name, ref mut new_dependency) = dep.to_toml();
table[name] = new_dependency.clone();
} else {
merge_dependencies(&mut table[&dep.name], dep);
if let Some(t) = table.as_inline_table_mut() {
t.fmt()
}
}
Ok(())
}
pub fn update_table_entry(
&mut self,
table_path: &[String],
dep: &Dependency,
dry_run: bool,
) -> Result<()> {
let table = self.get_table(table_path)?;
let new_dep = dep.to_toml().1;
if !table[&dep.name].is_none() {
if let Err(e) = print_upgrade_if_necessary(&dep.name, &table[&dep.name], &new_dep) {
eprintln!("Error while displaying upgrade message, {}", e);
}
if !dry_run {
merge_dependencies(&mut table[&dep.name], dep);
if let Some(t) = table.as_inline_table_mut() {
t.fmt()
}
}
}
Ok(())
}
pub fn remove_from_table(&mut self, table: &str, name: &str) -> Result<()> {
if !self.data[table].is_table_like() {
Err(ErrorKind::NonExistentTable(table.into()))?;
} else {
{
let dep = &mut self.data[table][name];
if dep.is_none() {
Err(ErrorKind::NonExistentDependency(name.into(), table.into()))?;
}
*dep = toml_edit::Item::None;
}
if self.data[table].as_table_like().unwrap().is_empty() {
self.data[table] = toml_edit::Item::None;
}
}
Ok(())
}
pub fn add_deps(&mut self, table: &[String], deps: &[Dependency]) -> Result<()> {
deps.iter()
.map(|dep| self.insert_into_table(table, dep))
.collect::<Result<Vec<_>>>()?;
Ok(())
}
}
impl str::FromStr for Manifest {
type Err = Error;
fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
let d: toml_edit::Document = input.parse().chain_err(|| "Manifest not valid TOML")?;
Ok(Manifest { data: d })
}
}
#[derive(Debug)]
pub struct LocalManifest {
path: PathBuf,
manifest: Manifest,
}
impl Deref for LocalManifest {
type Target = Manifest;
fn deref(&self) -> &Manifest {
&self.manifest
}
}
impl LocalManifest {
pub fn find(path: &Option<PathBuf>) -> Result<Self> {
let path = find(path)?;
Self::try_new(&path)
}
pub fn try_new(path: &Path) -> Result<Self> {
let path = path.to_path_buf();
Ok(LocalManifest {
manifest: Manifest::open(&Some(path.clone()))?,
path,
})
}
fn get_file(&self) -> Result<File> {
Manifest::find_file(&Some(self.path.clone()))
}
pub fn upgrade(&mut self, dependency: &Dependency, dry_run: bool) -> Result<()> {
for (table_path, table) in self.get_sections() {
let table_like = table.as_table_like().expect("Unexpected non-table");
for (name, _old_value) in table_like.iter() {
if name == dependency.name {
self.manifest
.update_table_entry(&table_path, dependency, dry_run)?;
}
}
}
let mut file = self.get_file()?;
self.write_to_file(&mut file)
.chain_err(|| "Failed to write new manifest contents")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dependency::Dependency;
use toml_edit;
#[test]
fn add_remove_dependency() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let clone = manifest.clone();
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
let _ = manifest.insert_into_table(&["dependencies".to_owned()], &dep);
assert!(manifest
.remove_from_table("dependencies", &dep.name)
.is_ok());
assert_eq!(manifest.data.to_string(), clone.data.to_string());
}
#[test]
fn update_dependency() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
manifest
.insert_into_table(&["dependencies".to_owned()], &dep)
.unwrap();
let new_dep = Dependency::new("cargo-edit").set_version("0.2.0");
manifest
.update_table_entry(&["dependencies".to_owned()], &new_dep, false)
.unwrap();
}
#[test]
fn update_wrong_dependency() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
manifest
.insert_into_table(&["dependencies".to_owned()], &dep)
.unwrap();
let original = manifest.clone();
let new_dep = Dependency::new("wrong-dep").set_version("0.2.0");
manifest
.update_table_entry(&["dependencies".to_owned()], &new_dep, false)
.unwrap();
assert_eq!(manifest.data.to_string(), original.data.to_string());
}
#[test]
fn remove_dependency_no_section() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
assert!(manifest
.remove_from_table("dependencies", &dep.name)
.is_err());
}
#[test]
fn remove_dependency_non_existent() {
let mut manifest = Manifest {
data: toml_edit::Document::new(),
};
let dep = Dependency::new("cargo-edit").set_version("0.1.0");
let other_dep = Dependency::new("other-dep").set_version("0.1.0");
let _ = manifest.insert_into_table(&["dependencies".to_owned()], &other_dep);
assert!(manifest
.remove_from_table("dependencies", &dep.name)
.is_err());
}
}