ros_add 0.1.3

The Purpose of the Package is to provide the `cargo ros_add` command to add dependencies to `Cargo.toml` and the `package.xml`
Documentation
use case_clause::case;
use clap::{Arg, ArgAction, ArgMatches, Command as ClapCommand};
use quick_xml::{
    NsReader, Writer,
    events::{BytesText, Event},
};
use std::{
    fs::{File, read_to_string, write},
    io::{BufReader, Cursor, ErrorKind},
};
use toml_edit::{DocumentMut, value};

/// Corresponding to http://download.ros.org/schema/package_format3.xsd this should cover all
/// package format 3 specific dependency types.
#[derive(Clone, Copy, Debug)]
pub enum DependencyType {
    /// A build dependency.
    BuildDepend,
    /// An exported build dependency.
    BuildExportDepend,
    /// A build tool dependency.
    BuildtoolDepend,
    /// An exported build tool dependency.
    BuildtoolExportDepend,
    /// An execution dependency.
    ExecDepend,
    /// A general dependency.
    Depend,
    /// A documentation dependency.
    DocDepend,
    /// A test dependency.
    TestDepend,
    /// A package conflict.
    Conflict,
    /// A package replacement.
    Replace,
}
impl DependencyType {
    /// Creates a new `DependencyType` from `clap::ArgMatches`.
    ///
    /// It checks which flag is set to determine the dependency type.
    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::TestDepend,
            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,
        )
    }
    /// Returns the corresponding TOML table name for a `Cargo.toml` file.
    pub fn cargo_toml_type(&self) -> &str {
        match self {
            DependencyType::BuildDepend => "build-dependencies",
            _ => "dependencies",
        }
    }

    /// Returns the corresponding XML element name for a `package.xml` file.
    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",
        }
    }
}

/// Converts command-line arguments into `clap::ArgMatches`.
///
/// This function defines the command-line interface for the `ros_add` tool,
/// including arguments for specifying dependencies and various dependency types.
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").action(ArgAction::SetTrue).help("package.xml only. What even is that?"))
        .arg(Arg::new("buildtool").action(ArgAction::SetTrue).help("package.xml only. These are for packages that provide the build infrastructure"))
        .arg(Arg::new("buildtool-export").action(ArgAction::SetTrue).help("package.xml only. What even is that?"))
        .arg(Arg::new("exec").action(ArgAction::SetTrue).help("package.xml only. Because Rust has no runtime applicable dependencies, this won't work"))
        .arg(Arg::new("doc").action(ArgAction::SetTrue).help("package.xml only. What even is that?"))
        .arg(Arg::new("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").action(ArgAction::SetTrue).help("package.xml only. What even is that?"))
        .arg(Arg::new("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
            )
        )
}

/// A struct to manage and modify a `Cargo.toml` document.
pub struct PathDoc {
    path: String,
    doc: DocumentMut,
    dependency_type: DependencyType,
}
impl PathDoc {
    /// Creates a new `PathDoc` instance by reading and parsing a `Cargo.toml` file.
    ///
    /// # Arguments
    ///
    /// * `path` - The path to the `Cargo.toml` file.
    /// * `dependency_type` - The type of dependency to add.
    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,
        })
    }

    /// Adds a dependency to the `Cargo.toml` document.
    ///
    /// # Arguments
    ///
    /// * `dependency_name` - The name of the dependency to add.
    pub fn add_dependency_to_cargo_toml(mut self, dependency_name: &str) -> Result<(), ErrorKind> {
        // Check if the table exists. If not, create it.
        if self
            .doc
            .get(self.dependency_type.cargo_toml_type())
            .is_none()
        {
            self.doc[self.dependency_type.cargo_toml_type()] = toml_edit::table();
        }

        // Now, safely get the table and check for the dependency.
        match self.doc[self.dependency_type.cargo_toml_type()].get(dependency_name) {
            Some(_) => {
                eprintln!("{dependency_name} already added to dependencies");
                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 {dependency_name} added to Cargo.toml");
                Ok(())
            }
        }
    }
}

/// A helper struct to manage and modify a `package.xml` document.
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 {
    /// Creates a new `XMLHelper` instance by reading and parsing a `package.xml` file.
    ///
    /// # Arguments
    ///
    /// * `path` - The path to the `package.xml` file.
    /// * `dependency_type` - The type of dependency to add.
    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,
        })
    }

    /// Adds a dependency to the `package.xml` document.
    ///
    /// This function iterates through the XML events, adding the new dependency
    /// before the `<export>` tag.
    ///
    /// # Arguments
    ///
    /// * `dependency_name` - The name of the dependency to add.
    pub fn add_dependency_to_package_xml(mut self, dependency_name: &str) -> 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 '{dependency_name}' already exists in package.xml"
                                );
                                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(())
    }
}