use case_clause::case;
use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command as ClapCommand};
use quick_xml::{de::from_str, se::Serializer};
use serde::{Deserialize, Serialize};
use std::{
env,
fmt::{self, Display},
fs,
io::ErrorKind,
path::{Path, PathBuf},
rc::Rc,
};
use toml_edit::{DocumentMut, Item};
struct InputString(Rc<str>);
struct PrefixString(Rc<str>);
struct SuffixString(Rc<str>);
pub struct StringHelper(PrefixString, InputString, SuffixString);
impl StringHelper {
#[must_use]
pub fn new(prefix: &str, input: &str, suffix: &str) -> Self {
Self(
PrefixString(prefix.into()),
InputString(input.into()),
SuffixString(suffix.into()),
)
}
#[must_use]
pub fn dependency(input: &str) -> String {
Self::new("Dependency will only be removed from ", input, " file.").to_string()
}
#[must_use]
pub fn section(input: &str) -> String {
Self::new("Remove from ", input, "-dependencies").to_string()
}
}
impl Display for StringHelper {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}{}", self.0.0, self.1.0, self.2.0)
}
}
#[must_use]
pub fn env_to_matches(env_args: Vec<String>) -> ArgMatches {
ClapCommand::new("ros_remove")
.about("removes dependencies from a Cargo.toml and package.xml manifest file")
.arg(
Arg::new("dependency")
.value_name("DEP_ID")
.help(
[
"Reference to a package that should be removed as a dependency.",
"You can reference a package by:",
"- `<name>`, like `cargo ros-remove serde` (latest version will be used)",
]
.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(StringHelper::dependency("package.xml"))
.long("no-cargo-toml")
.required(false),
)
.arg(
Arg::new("no_package_xml")
.action(ArgAction::SetTrue)
.help(StringHelper::dependency("Cargo.toml"))
.long("no-package-xml")
.required(false),
)
.group(ArgGroup::new("section").multiple(true))
.next_help_heading("Section")
.args([
Arg::new("build")
.action(ArgAction::SetTrue)
.help(StringHelper::section("build"))
.long("build"),
Arg::new("dev")
.action(ArgAction::SetTrue)
.help(StringHelper::section("dev"))
.long("dev"),
])
.get_matches_from(case!(
env_args.iter().any(|arg| arg == "ros-remove") => env_args.iter().take(1).chain(env_args.iter().skip(2)).cloned().collect::<Vec<String>>(),
true => env_args
))
}
fn remove_from_cargo_toml_helper(
mut doc: DocumentMut,
cargo_toml_path: &Path,
dependency: &str,
sections: Vec<&str>,
) -> Result<(), ErrorKind> {
let mut removed = false;
for section in sections {
if let Some(item) = doc
.get_mut(section)
.and_then(Item::as_table_mut)
.and_then(|deps| deps.remove(dependency))
{
println!("Removed `{dependency}` {item} from `{section}` in `Cargo.toml`");
removed = true;
}
}
if removed {
fs::write(cargo_toml_path, doc.to_string()).map_err(|err| err.kind())?;
}
Ok(())
}
pub fn remove_from_cargo_toml(
cargo_toml_path: &Path,
dependency: &str,
sections: Vec<&str>,
) -> Result<(), ErrorKind> {
remove_from_cargo_toml_helper(
fs::read_to_string(cargo_toml_path)
.map_err(|err| err.kind())?
.parse::<DocumentMut>()
.map_err(|_| ErrorKind::InvalidData)?,
cargo_toml_path,
dependency,
sections,
)
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename = "package")]
pub struct PackageXml {
#[serde(rename = "@format")]
pub format: String,
pub name: String,
pub version: String,
pub description: String,
pub maintainer: Vec<Maintainer>,
pub license: Vec<String>,
#[serde(rename = "depend", default, skip_serializing_if = "Option::is_none")]
pub depends: Option<Vec<String>>,
#[serde(
rename = "build_depend",
default,
skip_serializing_if = "Option::is_none"
)]
pub build_depends: Option<Vec<String>>,
#[serde(
rename = "exec_depend",
default,
skip_serializing_if = "Option::is_none"
)]
pub exec_depends: Option<Vec<String>>,
#[serde(
rename = "test_depend",
default,
skip_serializing_if = "Option::is_none"
)]
pub test_depends: Option<Vec<String>>,
#[serde(
rename = "doc_depend",
default,
skip_serializing_if = "Option::is_none"
)]
pub doc_depends: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub export: Option<Export>,
}
impl PackageXml {
fn new(path: &Path) -> Result<Self, ErrorKind> {
fs::read_to_string(path)
.map_err(|err| err.kind())
.map(|string| from_str(&string).map_err(|_| ErrorKind::InvalidData))?
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Maintainer {
#[serde(rename = "@email")]
pub email: String,
#[serde(rename = "$value")] pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Export {
pub build_type: String,
}
fn process_list(list: &mut Option<Vec<String>>, dependency: &str) -> bool {
if let Some(deps) = list {
let initial_count = deps.len();
deps.retain(|dep| dep != dependency);
deps.len() != initial_count
} else {
false
}
}
pub fn remove_from_package_xml(package_xml_path: &Path, dependency: &str) -> Result<(), ErrorKind> {
let mut package: PackageXml = PackageXml::new(package_xml_path)?;
if [
&mut package.depends,
&mut package.build_depends,
&mut package.exec_depends,
&mut package.doc_depends,
&mut package.test_depends,
]
.iter_mut()
.any(|field| process_list(field, dependency))
{
let mut buffer = String::new();
let mut ser = Serializer::new(&mut buffer);
ser.indent(' ', 2);
package
.serialize(ser)
.map_err(|_| ErrorKind::InvalidInput)?;
fs::write(package_xml_path, buffer).map_err(|err| err.kind())?;
println!("Removed `{dependency}` from `package.xml` (formatting preserved)");
}
Ok(())
}
fn remove_dependency_from_file(
file_name: &str,
dependency: &str,
sections: Vec<&str>,
) -> Result<(), ErrorKind> {
let file_path: PathBuf = env::current_dir()
.map_err(|err| err.kind())?
.join(file_name);
if file_path.exists() || file_path.is_file() {
match file_name {
"Cargo.toml" => {
remove_from_cargo_toml(&file_path, dependency, sections)?;
}
"package.xml" => {
remove_from_package_xml(&file_path, dependency)?;
}
_ => {
eprintln!("Warning: Unsupported file type: {file_name}");
return Err(ErrorKind::InvalidFilename);
}
}
Ok(())
} else {
eprintln!("Warning: `{file_name}` not found in the current directory");
Err(ErrorKind::NotFound)
}
}
pub fn remove_dependency(matches: &ArgMatches) -> Result<(), ErrorKind> {
let dependency: &String = matches.get_one::<String>("dependency").ok_or_else(|| {
eprintln!("Dependency name is required");
ErrorKind::InvalidInput
})?;
let mut sections: Vec<&str> = Vec::new();
if !matches.get_flag("build") && !matches.get_flag("dev") {
sections.push("dependencies");
} else {
for (key, val) in [("build", "build-dependencies"), ("dev", "dev-dependencies")] {
if matches.get_flag(key) {
sections.push(val);
}
}
}
for val in [
("no_cargo_toml", "Cargo.toml"),
("no_package_xml", "package.xml"),
]
.iter()
.filter(|key| !matches.get_flag(key.0))
.map(|key| key.1)
{
remove_dependency_from_file(val, dependency, sections.clone())?;
}
Ok(())
}