use case_clause::case;
use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand};
use quick_xml::{
NsReader, Writer,
events::{BytesText, Event},
};
use std::{
fmt::{self, Display, Formatter},
fs::{File, read_to_string, write},
io::{BufReader, Cursor, ErrorKind},
};
use toml_edit::{DocumentMut, value};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DependencyType {
BuildDepend,
BuildExportDepend,
BuildtoolDepend,
BuildtoolExportDepend,
ExecDepend,
Depend,
DocDepend,
TestDepend,
Conflict,
Replace,
}
impl DependencyType {
pub fn new(flag: ArgMatches) -> Self {
case! (
flag.get_flag("build")=> DependencyType::BuildDepend,
flag.get_flag("build-export") => DependencyType::BuildExportDepend,
flag.get_flag("buildtool") => DependencyType::BuildtoolDepend,
flag.get_flag("buildtool-export") => DependencyType::BuildtoolExportDepend,
flag.get_flag("exec") => DependencyType::ExecDepend, flag.get_flag("doc") => DependencyType::DocDepend,
flag.get_flag("test") => DependencyType::TestDepend,
flag.get_flag("conflict") => DependencyType::Conflict,
flag.get_flag("replace") => DependencyType::Replace,
true => DependencyType::Depend,
)
}
pub fn cargo_toml_type(&self) -> &str {
match self {
DependencyType::BuildDepend => "build-dependencies",
_ => "dependencies",
}
}
pub fn package_xml_type(&self) -> &str {
match self {
DependencyType::Replace => "replace",
DependencyType::Conflict => "conflict",
DependencyType::TestDepend => "test-depend",
DependencyType::DocDepend => "doc-depend",
DependencyType::BuildtoolDepend => "buildtool-depend",
DependencyType::ExecDepend => "exec-depend",
DependencyType::BuildtoolExportDepend => "build_export_depend",
DependencyType::BuildDepend => "build_depend",
DependencyType::BuildExportDepend => "build_export_depend",
DependencyType::Depend => "depend",
}
}
}
pub fn env_to_matches(env_args: Vec<String>) -> ArgMatches {
ClapCommand::new("ros_add")
.version("0.1.0")
.author("GueLaKais <koroyeldiores@gmail.com>")
.about("Add dependencies to a Cargo.toml and package.xml manifest file")
.arg(
Arg::new("dependency").value_name("DEP_ID")
.help(["Reference to a package to add as a dependency","You can reference a package by:","- `<name>`, like `cargo ros-add serde` (latest version will be used)","- `<name>@<version-req>`, like `cargo add serde@1` or `cargo add serde@=1.0.38`"].join("\n")).required(true)
)
.arg(Arg::new("color").help("Coloring\n \n").long("color").value_name("WHEN").value_parser(["auto", "always", "never"]))
.arg(Arg::new("no_cargo_toml").action(ArgAction::SetTrue).help("Dependency will only be added to package.xml file.").long("no-cargo-toml").required(false))
.arg(Arg::new("no_package_xml").action(ArgAction::SetTrue).help("Dependency will only be added to the Cargo.toml file.").long("no-package-xml").required(false))
.arg(Arg::new("build").action(ArgAction::SetTrue).help(["add as build dependency","will appear in Cargo.toml as [build-dependencies] and in package.xml as build_depend element","Build-dependencies are only dependencies available for use by build scripts (`build.rs` files)."].join("\n")).long("build"))
.arg(Arg::new("build-export").long("build-export").action(ArgAction::SetTrue).help("package.xml only. What even is that?"))
.arg(Arg::new("buildtool").long("buildtool").action(ArgAction::SetTrue).help("package.xml only. These are for packages that provide the build infrastructure"))
.arg(Arg::new("buildtool-export").long("buildtool-export").action(ArgAction::SetTrue).help("package.xml only. What even is that?"))
.arg(Arg::new("exec").long("exec").action(ArgAction::SetTrue).help("package.xml only. Because Rust has no runtime applicable dependencies, this won't work"))
.arg(Arg::new("doc").long("doc").action(ArgAction::SetTrue).help("package.xml only. What even is that?"))
.arg(Arg::new("test").long("test").action(ArgAction::SetTrue).help("only in package.xml a different dependency type. Will appear in the Cargo.toml as standard dependency"))
.arg(Arg::new("conflict").long("conflict").action(ArgAction::SetTrue).help("package.xml only. What even is that?"))
.arg(Arg::new("replace").long("replace").action(ArgAction::SetTrue).help("package.xml only. What even is that?"))
.get_matches_from(
case!(
env_args.iter().any(|arg| arg == "ros-add") => env_args.iter().take(1).chain(env_args.iter().skip(2)).cloned().collect::<Vec<String>>(),
true => env_args
)
)
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq)]
pub struct PackageName<'a> {
pub name: &'a str,
pub version: Option<&'a str>,
}
impl<'a> PackageName<'a> {
pub fn new(raw_dependency: &'a str) -> PackageName<'a> {
let mut parts = raw_dependency.split('@');
Self {
name: parts.next().unwrap_or(raw_dependency),
version: parts.next(),
}
}
}
impl<'a> Display for PackageName<'a> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"Package:{}{}",
self.name,
self.version
.map_or_else(|| "".to_string(), |some| format!(",version:{}", some))
)
}
}
pub struct PathDoc {
path: String,
doc: DocumentMut,
dependency_type: DependencyType,
}
impl PathDoc {
pub fn new(path: String, dependency_type: DependencyType) -> Result<PathDoc, ErrorKind> {
Ok(PathDoc {
path: path.clone(),
doc: read_to_string(path.as_str())
.map_err(|e| e.kind())?
.parse::<DocumentMut>()
.map_err(|_| ErrorKind::InvalidData)?,
dependency_type,
})
}
pub fn add_dependency_to_cargo_toml(
mut self,
dependency: PackageName,
) -> Result<(), ErrorKind> {
if self
.doc
.get(self.dependency_type.cargo_toml_type())
.is_none()
{
self.doc[self.dependency_type.cargo_toml_type()] = toml_edit::table();
}
match self.doc[self.dependency_type.cargo_toml_type()].get(dependency.name) {
Some(_) => {
eprintln!("{} already added to dependencies", dependency.name);
Err(ErrorKind::AlreadyExists)
}
None => {
self.doc[self.dependency_type.cargo_toml_type()][dependency.name] = value("*");
write(self.path.as_str(), self.doc.to_string()).map_err(|e| e.kind())?;
println!("Dependency {} added to Cargo.toml", dependency.name);
Ok(())
}
}
}
}
pub struct XMLHelper {
pub path: String,
pub reader: NsReader<BufReader<File>>,
pub writer: Writer<Cursor<Vec<u8>>>,
pub buf: Vec<u8>,
dependency_type: DependencyType,
}
impl XMLHelper {
pub fn new(path: String, dependency_type: DependencyType) -> Result<XMLHelper, ErrorKind> {
Ok(XMLHelper {
path: path.clone(),
reader: NsReader::from_file(&path).map_err(|_| ErrorKind::NotFound)?,
writer: Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 2),
buf: Vec::new(),
dependency_type,
})
}
pub fn add_dependency_to_package_xml(
mut self,
dependency: PackageName,
) -> Result<(), ErrorKind> {
self.reader.config_mut().trim_text(true);
loop {
self.buf.clear();
match self.reader.read_event_into(&mut self.buf) {
Ok(Event::Start(e))
if e.name().as_ref() == self.dependency_type.package_xml_type().as_bytes() =>
{
self.writer
.write_event(Event::Start(e.clone()))
.map_err(|_| ErrorKind::WriteZero)?;
match self.reader.read_event_into(&mut self.buf) {
Ok(Event::Text(text)) => {
if self
.reader
.decoder()
.decode(text.as_ref())
.unwrap_or_default()
== dependency.name
{
eprintln!(
"Dependency '{}' already exists in package.xml",
dependency.name
);
return Err(ErrorKind::AlreadyExists);
}
self.writer
.write_event(Event::Text(text))
.map_err(|_| ErrorKind::WriteZero)?;
}
_ => {
println!("Everything allright")
}
}
}
Ok(Event::Start(e)) if e.name().as_ref() == b"export" => {
self.writer
.create_element(self.dependency_type.package_xml_type())
.write_text_content(BytesText::new(dependency.name))
.map_err(|_| ErrorKind::OutOfMemory)?;
self.writer
.write_event(Event::Start(e))
.map_err(|_| ErrorKind::WriteZero)?;
}
Ok(Event::Eof) => break,
Ok(e) => self
.writer
.write_event(e)
.map_err(|_| ErrorKind::WriteZero)?,
Err(_) => return Err(ErrorKind::InvalidData),
}
}
write(&self.path, self.writer.into_inner().into_inner()).map_err(|e| e.kind())?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs, io::Write};
use tempfile::NamedTempFile;
fn create_mock_matches(flags: Vec<&str>) -> ArgMatches {
let command = ClapCommand::new("test_app")
.arg(Arg::new("build").long("build").action(ArgAction::SetTrue))
.arg(
Arg::new("build-export")
.long("build-export")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("buildtool")
.long("buildtool")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("buildtool-export")
.long("buildtool-export")
.action(ArgAction::SetTrue),
)
.arg(Arg::new("exec").long("exec").action(ArgAction::SetTrue))
.arg(Arg::new("doc").long("doc").action(ArgAction::SetTrue))
.arg(Arg::new("test").long("test").action(ArgAction::SetTrue))
.arg(
Arg::new("conflict")
.long("conflict")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new("replace")
.long("replace")
.action(ArgAction::SetTrue),
);
let mut args = vec!["test_app"];
args.extend(flags);
command.get_matches_from(args)
}
#[test]
fn test_package_name_new() {
assert_eq!(PackageName::new("serde@1.0").name, "serde");
assert_eq!(PackageName::new("serde").name, "serde");
assert_eq!(PackageName::new("tokio@^1").name, "tokio");
}
#[test]
fn test_dependency_type_new() {
assert_eq!(
DependencyType::new(create_mock_matches(vec!["--build"])),
DependencyType::BuildDepend
);
assert_eq!(
DependencyType::new(create_mock_matches(vec!["--build-export"])),
DependencyType::BuildExportDepend
);
assert_eq!(
DependencyType::new(create_mock_matches(vec!["--buildtool"])),
DependencyType::BuildtoolDepend
);
assert_eq!(
DependencyType::new(create_mock_matches(vec!["--buildtool-export"])),
DependencyType::BuildtoolExportDepend
);
assert_eq!(
DependencyType::new(create_mock_matches(vec!["--exec"])),
DependencyType::ExecDepend
);
assert_eq!(
DependencyType::new(create_mock_matches(vec!["--doc"])),
DependencyType::DocDepend
);
assert_eq!(
DependencyType::new(create_mock_matches(vec!["--test"])),
DependencyType::TestDepend
);
assert_eq!(
DependencyType::new(create_mock_matches(vec!["--conflict"])),
DependencyType::Conflict
);
assert_eq!(
DependencyType::new(create_mock_matches(vec!["--replace"])),
DependencyType::Replace
);
assert_eq!(
DependencyType::new(create_mock_matches(vec![])),
DependencyType::Depend
);
}
#[test]
fn test_dependency_type_cargo_toml_type() {
assert_eq!(
DependencyType::BuildDepend.cargo_toml_type(),
"build-dependencies"
);
assert_eq!(DependencyType::Depend.cargo_toml_type(), "dependencies");
assert_eq!(DependencyType::TestDepend.cargo_toml_type(), "dependencies");
}
#[test]
fn test_dependency_type_package_xml_type() {
assert_eq!(DependencyType::Replace.package_xml_type(), "replace");
assert_eq!(DependencyType::Conflict.package_xml_type(), "conflict");
assert_eq!(DependencyType::TestDepend.package_xml_type(), "test-depend");
assert_eq!(DependencyType::DocDepend.package_xml_type(), "doc-depend");
assert_eq!(
DependencyType::BuildtoolDepend.package_xml_type(),
"buildtool-depend"
);
assert_eq!(DependencyType::ExecDepend.package_xml_type(), "exec-depend");
assert_eq!(
DependencyType::BuildtoolExportDepend.package_xml_type(),
"build_export_depend"
);
assert_eq!(
DependencyType::BuildDepend.package_xml_type(),
"build_depend"
);
assert_eq!(
DependencyType::BuildExportDepend.package_xml_type(),
"build_export_depend"
);
assert_eq!(DependencyType::Depend.package_xml_type(), "depend");
}
#[test]
fn test_add_dependency_to_cargo_toml_success() {
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "[package]\nname = \"test\"\nversion = \"0.1.0\"").unwrap();
let path = file.path().to_str().unwrap().to_string();
let path_doc = PathDoc::new(path.clone(), DependencyType::Depend).unwrap();
let result = path_doc.add_dependency_to_cargo_toml(PackageName::new("serde"));
assert!(result.is_ok());
let content = fs::read_to_string(path).unwrap();
assert!(content.contains("[dependencies]"));
assert!(content.contains("serde = \"*\""));
}
#[test]
fn test_add_dependency_to_cargo_toml_already_exists() {
let mut file = NamedTempFile::new().unwrap();
writeln!(
file,
"[package]\nname = \"test\"\nversion = \"0.1.0\"\n[dependencies]\nserde = \"*\""
)
.unwrap();
let path = file.path().to_str().unwrap().to_string();
let path_doc = PathDoc::new(path, DependencyType::Depend).unwrap();
let result = path_doc.add_dependency_to_cargo_toml(PackageName::new("serde"));
assert_eq!(result.err(), Some(ErrorKind::AlreadyExists));
}
#[test]
fn test_add_dependency_to_package_xml_success() {
let mut file = NamedTempFile::new().unwrap();
writeln!(
file,
"<package format=\"3\">\n <name>test_pkg</name>\n <version>0.0.0</version>\n <description>TODO</description>\n <maintainer email=\"test@test.com\">Test</maintainer>\n <license>TODO</license>\n <export>\n <build_type>ament_cmake</build_type>\n </export>\n</package>"
)
.unwrap();
let path = file.path().to_str().unwrap().to_string();
let xml_helper = XMLHelper::new(path.clone(), DependencyType::Depend).unwrap();
let result = xml_helper.add_dependency_to_package_xml(PackageName::new("rclcpp"));
assert!(result.is_ok());
let content = fs::read_to_string(path).unwrap();
assert!(content.contains("<depend>rclcpp</depend>"));
}
#[test]
fn test_add_dependency_to_package_xml_already_exists() {
let mut file = NamedTempFile::new().unwrap();
writeln!(
file,
"<package format=\"3\">\n <name>test_pkg</name>\n <depend>rclcpp</depend>\n <export>\n </export>\n</package>"
)
.unwrap();
let path = file.path().to_str().unwrap().to_string();
let xml_helper = XMLHelper::new(path, DependencyType::Depend).unwrap();
let result = xml_helper.add_dependency_to_package_xml(PackageName::new("rclcpp"));
assert_eq!(result.err(), Some(ErrorKind::AlreadyExists));
}
}