use anyhow::{Context, Result};
use std::path::Path;
use std::process::Command;
pub fn handle_add_command(
package: &str,
version: Option<&str>,
dev: bool,
verbose: bool,
) -> Result<()> {
verify_cargo_project()?;
run_cargo_add(package, version, dev, verbose)?;
print_success_message(package, version, dev);
Ok(())
}
fn verify_cargo_project() -> Result<()> {
let cargo_toml = Path::new("Cargo.toml");
if !cargo_toml.exists() {
anyhow::bail!(
"Cargo.toml not found. Run this command from a Cargo project directory.\n\
Hint: Use `ruchy new <name>` to create a new Ruchy project."
);
}
Ok(())
}
fn run_cargo_add(package: &str, version: Option<&str>, dev: bool, verbose: bool) -> Result<()> {
let mut cmd = Command::new("cargo");
cmd.arg("add");
if let Some(ver) = version {
cmd.arg(format!("{package}@{ver}"));
} else {
cmd.arg(package);
}
if dev {
cmd.arg("--dev");
}
if verbose {
let dev_flag = if dev { " --dev" } else { "" };
let version_flag = version.map_or(String::new(), |v| format!("@{v}"));
println!("Running: cargo add {package}{version_flag}{dev_flag}");
}
let output = cmd
.output()
.context("Failed to run cargo add - ensure cargo is installed")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("cargo add failed: {stderr}");
}
if verbose && !output.stdout.is_empty() {
let stdout = String::from_utf8_lossy(&output.stdout);
println!("{stdout}");
}
Ok(())
}
fn print_success_message(package: &str, version: Option<&str>, dev: bool) {
let dep_type = if dev { "dev-dependency" } else { "dependency" };
let version_str = version.map_or(String::new(), |v| format!(" (version {v})"));
println!("Added {package}{version_str} as {dep_type}");
println!("Run `cargo build` to compile with the new dependency");
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::fs;
use tempfile::TempDir;
fn create_test_project() -> (TempDir, String) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project_name = "test_project";
let project_path = temp_dir.path().join(project_name);
let cargo_toml_content = r#"[package]
name = "test_project"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
fs::create_dir(&project_path).expect("Failed to create project dir");
fs::write(project_path.join("Cargo.toml"), cargo_toml_content)
.expect("Failed to write Cargo.toml");
(
temp_dir,
project_path
.to_str()
.expect("Project path should be valid UTF-8")
.to_string(),
)
}
#[test]
fn test_verify_cargo_project_missing_cargo_toml() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let _original_dir = env::current_dir().expect("Failed to get current dir");
env::set_current_dir(temp_dir.path()).expect("Failed to change dir");
let result = verify_cargo_project();
env::set_current_dir(_original_dir).expect("Failed to restore dir");
assert!(result.is_err(), "Should fail when Cargo.toml doesn't exist");
assert!(
result
.unwrap_err()
.to_string()
.contains("Cargo.toml not found"),
"Error message should mention Cargo.toml"
);
}
#[test]
fn test_verify_cargo_project_success() {
let (_temp_dir, project_path) = create_test_project();
let _original_dir = env::current_dir().expect("Failed to get current dir");
env::set_current_dir(&project_path).expect("Failed to change dir");
let result = verify_cargo_project();
env::set_current_dir(_original_dir).expect("Failed to restore dir");
assert!(result.is_ok(), "Should succeed when Cargo.toml exists");
}
#[test]
fn test_print_success_message_basic() {
print_success_message("serde", None, false);
}
#[test]
fn test_print_success_message_with_version() {
print_success_message("tokio", Some("1.0"), false);
}
#[test]
fn test_print_success_message_dev_dependency() {
print_success_message("proptest", Some("1.0"), true);
}
#[test]
fn test_verify_cargo_project_idempotent() {
let (_temp_dir, project_path) = create_test_project();
let _original_dir = env::current_dir().expect("Failed to get current dir");
env::set_current_dir(&project_path).expect("Failed to change dir");
let result1 = verify_cargo_project();
let result2 = verify_cargo_project();
env::set_current_dir(_original_dir).expect("Failed to restore dir");
assert_eq!(
result1.is_ok(),
result2.is_ok(),
"Multiple calls should have same result"
);
}
#[test]
fn test_print_success_message_all_combinations() {
print_success_message("pkg", None, false);
print_success_message("pkg", None, true);
print_success_message("pkg", Some("1.0"), false);
print_success_message("pkg", Some("1.0"), true);
}
#[test]
fn test_handle_add_command_no_cargo_toml() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let _original_dir = env::current_dir().expect("Failed to get current dir");
env::set_current_dir(temp_dir.path()).expect("Failed to change dir");
let result = handle_add_command("serde", None, false, false);
env::set_current_dir(_original_dir).expect("Failed to restore dir");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cargo.toml not found"));
}
#[test]
fn test_handle_add_command_verbose() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let _original_dir = env::current_dir().expect("Failed to get current dir");
env::set_current_dir(temp_dir.path()).expect("Failed to change dir");
let result = handle_add_command("serde", None, false, true);
env::set_current_dir(_original_dir).expect("Failed to restore dir");
assert!(result.is_err());
}
#[test]
fn test_handle_add_command_dev_flag() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let _original_dir = env::current_dir().expect("Failed to get current dir");
env::set_current_dir(temp_dir.path()).expect("Failed to change dir");
let result = handle_add_command("proptest", Some("1.0"), true, false);
env::set_current_dir(_original_dir).expect("Failed to restore dir");
assert!(result.is_err());
}
#[test]
fn test_print_success_message_various_packages() {
let packages = ["serde", "tokio", "anyhow", "thiserror", "tracing"];
for pkg in &packages {
print_success_message(pkg, None, false);
print_success_message(pkg, Some("0.1"), true);
}
}
#[test]
fn test_print_success_message_version_formats() {
let versions = ["1.0", "^1.0", "~1.0", ">=1.0", "0.1.0-beta", "*"];
for ver in &versions {
print_success_message("pkg", Some(ver), false);
}
}
#[test]
fn test_verify_cargo_project_in_nested_dir() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let _original_dir = env::current_dir().expect("Failed to get current dir");
let nested = temp_dir.path().join("nested").join("deep");
fs::create_dir_all(&nested).expect("Failed to create nested dirs");
env::set_current_dir(&nested).expect("Failed to change dir");
let result = verify_cargo_project();
env::set_current_dir(_original_dir).expect("Failed to restore dir");
assert!(result.is_err());
}
#[test]
fn test_handle_add_all_parameters() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let _original_dir = env::current_dir().expect("Failed to get current dir");
env::set_current_dir(temp_dir.path()).expect("Failed to change dir");
let _ = handle_add_command("pkg", None, false, false);
let _ = handle_add_command("pkg", None, true, false);
let _ = handle_add_command("pkg", Some("1"), false, true);
let _ = handle_add_command("pkg", Some("1"), true, true);
env::set_current_dir(_original_dir).expect("Failed to restore dir");
}
#[test]
fn test_create_test_project_structure() {
let (temp_dir, project_path) = create_test_project();
let cargo_path = Path::new(&project_path).join("Cargo.toml");
assert!(cargo_path.exists(), "Cargo.toml should exist");
let content = fs::read_to_string(&cargo_path).expect("Should read");
assert!(content.contains("[package]"));
assert!(content.contains("test_project"));
drop(temp_dir);
}
}