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},
rc::Rc,
str::Split,
};
use toml_edit::DocumentMut;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PartialOrd)]
pub enum DependencyType {
BuildDepend,
BuildExportDepend,
BuildtoolDepend,
BuildtoolExportDepend,
ExecDepend,
#[default]
Depend,
DocDepend,
TestDepend,
Conflict,
Replace,
}
impl DependencyType {
#[must_use]
pub fn cargo_toml_type(&self) -> &str {
match self {
Self::BuildDepend => "build-dependencies",
_ => "dependencies",
}
}
#[must_use]
pub fn package_xml_type(&self) -> &str {
match self {
Self::Replace => "replace",
Self::Conflict => "conflict",
Self::TestDepend => "test-depend",
Self::DocDepend => "doc-depend",
Self::BuildtoolDepend => "buildtool-depend",
Self::ExecDepend => "exec-depend",
Self::BuildtoolExportDepend => "buildtool_export_depend",
Self::BuildDepend => "build_depend",
Self::BuildExportDepend => "build_export_depend",
Self::Depend => "depend",
}
}
}
impl From<ArgMatches> for DependencyType {
fn from(item: ArgMatches) -> Self {
case! (
item.get_flag("build")=> Self::BuildDepend,
item.get_flag("build-export") => Self::BuildExportDepend,
item.get_flag("buildtool") => Self::BuildtoolDepend,
item.get_flag("buildtool-export") => Self::BuildtoolExportDepend,
item.get_flag("exec") => Self::ExecDepend,
item.get_flag("doc") => Self::DocDepend,
item.get_flag("test") => Self::TestDepend,
item.get_flag("conflict") => Self::Conflict,
item.get_flag("replace") => Self::Replace,
true => Self::Depend,
)
}
}
#[must_use]
pub fn env_to_matches(env_args: Vec<String>) -> ArgMatches {
ClapCommand::new("ros_add")
.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(Clone, Debug)]
pub struct SplitRaw<'a> {
parts: Split<'a, char>,
raw: Rc<str>,
}
impl<'a> From<&'a str> for SplitRaw<'a> {
fn from(item: &'a str) -> Self {
Self {
parts: item.split('@'),
raw: item.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq)]
pub struct PackageName {
pub name: Rc<str>,
pub version: Rc<str>,
}
impl Display for PackageName {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "Package:{}, Version:{}", self.name, self.version)
}
}
impl<'a> From<SplitRaw<'a>> for PackageName {
fn from(mut item: SplitRaw<'a>) -> Self {
Self {
name: item.parts.next().unwrap_or(item.raw.as_ref()).into(),
version: item.parts.next().unwrap_or("*").into(),
}
}
}
impl<'a> From<&'a str> for PackageName {
fn from(item: &'a str) -> Self {
SplitRaw::from(item).into()
}
}
#[derive(Debug, Clone)]
pub struct PathDoc {
path: String,
doc: DocumentMut,
dependency_type: DependencyType,
}
impl PathDoc {
pub fn new(path: &str, dependency_type: DependencyType) -> Result<Self, ErrorKind> {
Ok(Self {
path: path.to_string(),
doc: read_to_string(path)
.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();
}
#[allow(clippy::single_match_else)]
match self.doc[self.dependency_type.cargo_toml_type()].get(dependency.name.as_ref()) {
Some(some) => {
eprintln!("{some} already added to dependencies");
Err(ErrorKind::AlreadyExists)
}
None => {
self.doc[self.dependency_type.cargo_toml_type()][dependency.name.as_ref()] =
toml_edit::value(dependency.version.as_ref());
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: &str, dependency_type: DependencyType) -> Result<Self, ErrorKind> {
Ok(Self {
path: path.to_string(),
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)) => {
case!(
self.reader.decoder().decode(text.as_ref()).unwrap_or_default() == dependency.name.as_ref() => {eprintln!("Dependency '{}' already exists in package.xml",dependency.name);return Err(ErrorKind::AlreadyExists);},
true => 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.as_ref()))
.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(e) => {
eprintln!("XML parsing error: {e}");
return Err(ErrorKind::InvalidData);
}
}
}
write(&self.path, self.writer.into_inner().into_inner()).map_err(|e| e.kind())?;
Ok(())
}
}