ros_add 0.1.2

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};

/// Parses command-line arguments for the `ros-add` utility.
///
/// This function configures and executes a `clap` command parser to handle
/// arguments provided to the application. It is designed to be called directly
/// from `cargo` as `cargo ros-add`, and it adjusts the arguments accordingly
/// by removing `ros-add` to make `clap` parse them correctly.
///
/// # Arguments
///
/// * `env_args` - A `Vec<String>` of arguments, typically from `std::env::args()`.
///
/// # Returns
///
/// * `ArgMatches` - The parsed and matched command-line arguments.
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")
                .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"))
                .index(1)
                .long("DEP_ID")
                .required(true)
                .value_name("DEP"), 
        )
        .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))
        .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 helper struct for reading and writing XML files.
///
/// It encapsulates a `quick_xml::NsReader` for reading and a `quick_xml::Writer`
/// for writing, along with the file path and necessary buffers.
pub struct XMLHelper {
    /// The path to the XML file.
    pub path: String,
    /// The XML reader.
    pub reader: NsReader<BufReader<File>>,
    /// The XML writer.
    pub writer: Writer<Cursor<Vec<u8>>>,
    /// A buffer used for reading XML events.
    pub buf: Vec<u8>,
}

impl XMLHelper {
    /// Creates a new `XMLHelper` instance for a given file path.
    ///
    /// # Arguments
    ///
    /// * `path` - A `String` representing the path to the `package.xml` file.
    ///
    /// # Returns
    ///
    /// * `Result<XMLHelper, ErrorKind>` - An `XMLHelper` on success, or an `ErrorKind` on failure.
    ///   Returns `ErrorKind::NotFound` if the file cannot be opened.
    pub fn new(path: String) -> 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(),
        })
    }
}

/// Adds a `<depend>` tag to a `package.xml` file.
///
/// This function iterates through the XML events of a `package.xml` file.
/// It checks if the dependency already exists. If not, it inserts a new
/// `<depend>dependency_name</depend>` element just before the `<export>` tag.
///
/// # Arguments
///
/// * `path` - The path to the `package.xml` file to be modified.
/// * `reader` - An `NsReader` to read the XML file.
/// * `writer` - A `Writer` to write the modified XML content.
/// * `buf` - A buffer for the reader.
/// * `dependency_name` - The name of the dependency to add.
///
/// # Returns
///
/// * `Result<(), ErrorKind>` - `Ok(())` on success, or an `ErrorKind` on failure.
///   - `ErrorKind::AlreadyExists` if the dependency is already in the file.
///   - `ErrorKind::WriteZero` if writing to the buffer fails.
///   - `ErrorKind::InvalidData` if the XML data is malformed.
pub fn add_dependency_to_package_xml(
    path: String,
    mut reader: NsReader<BufReader<File>>,
    mut writer: Writer<Cursor<Vec<u8>>>,
    mut buf: Vec<u8>,
    dependency_name: &str,
) -> Result<(), ErrorKind> {
    reader.config_mut().trim_text(true);

    loop {
        buf.clear();
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(e)) if e.name().as_ref() == b"depend" => {
                writer
                    .write_event(Event::Start(e.clone()))
                    .map_err(|_| ErrorKind::WriteZero)?;
                match reader.read_event_into(&mut buf) {
                    Ok(Event::Text(text)) => {
                        if reader.decoder().decode(text.as_ref()).unwrap_or_default()
                            == dependency_name
                        {
                            eprintln!(
                                "Dependency '{dependency_name}' already exists in package.xml"
                            );
                            return Err(ErrorKind::AlreadyExists);
                        }

                        writer
                            .write_event(Event::Text(text))
                            .map_err(|_| ErrorKind::WriteZero)?;
                    }
                    _ => {
                        println!("Everything allright")
                    }
                }
            }
            Ok(Event::Start(e)) if e.name().as_ref() == b"export" => {
                writer
                    .create_element("depend")
                    .write_text_content(BytesText::new(dependency_name))
                    .map_err(|_| ErrorKind::OutOfMemory)?;

                writer
                    .write_event(Event::Start(e))
                    .map_err(|_| ErrorKind::WriteZero)?;
            }
            Ok(Event::Eof) => break,
            Ok(e) => writer.write_event(e).map_err(|_| ErrorKind::WriteZero)?,
            Err(_) => return Err(ErrorKind::InvalidData),
        }
    }

    write(&path, writer.into_inner().into_inner()).map_err(|e| e.kind())?;
    Ok(())
}

/// Reads a file and parses it into a mutable TOML document.
///
/// # Arguments
///
/// * `path` - The `String` path to the TOML file (e.g., "Cargo.toml").
///
/// # Returns
///
/// * `Result<(String, DocumentMut), ErrorKind>` - A tuple containing the original path and the
///   parsed `DocumentMut` on success, or an `ErrorKind` on failure.
///   - `ErrorKind::InvalidData` if the file content is not valid TOML.
///   - Other `ErrorKind` variants if the file cannot be read.
pub fn path_to_document(path: String) -> Result<(String, DocumentMut), ErrorKind> {
    Ok((
        path.clone(),
        read_to_string(path.as_str())
            .map_err(|e| e.kind())?
            .parse::<DocumentMut>()
            .map_err(|_| ErrorKind::InvalidData)?,
    ))
}

/// Adds a dependency to a `Cargo.toml` document under a specified section.
///
/// This function checks if the dependency already exists in the specified table
/// (e.g., `[dependencies]`). If it does not, it adds the dependency with a wildcard
/// version ("*") and writes the updated document back to the file.
///
/// # Arguments
///
/// * `path` - The path to the `Cargo.toml` file.
/// * `location` - The TOML table to add the dependency to (e.g., "dependencies").
/// * `doc` - The `DocumentMut` representing the parsed `Cargo.toml`.
/// * `dependency_name` - The name of the dependency to add.
///
/// # Returns
///
/// * `Result<(), ErrorKind>` - `Ok(())` on success, or an `ErrorKind` on failure.
///   - `ErrorKind::AlreadyExists` if the dependency is already in the file.
///   - Other `ErrorKind` variants if writing to the file fails.
pub fn add_dependency_to_cargo_toml(
    path: String,
    location: String,
    mut doc: DocumentMut,
    dependency_name: &str,
) -> Result<(), ErrorKind> {
    match doc[location.as_str()].get(dependency_name) {
        Some(_) => {
            eprintln!("Dependency '{dependency_name}' already exists in Cargo.toml");
            Err(ErrorKind::AlreadyExists)
        }
        None => {
            doc[location.as_str()][dependency_name] = value("*");
            write(path.as_str(), doc.to_string()).map_err(|e| e.kind())?;
            Ok(())
        }
    }
}