use std::path::Path;
use std::process::Command;
use anyhow::{anyhow, Context, Result};
use colored::Colorize;
#[allow(unused_imports)]
use toml_edit::{DocumentMut, Item};
pub fn write_cargo_toml(project_dir: &Path) -> anyhow::Result<()> {
let cargo_toml = format!(
r#"[package]
name = "{}"
version = "0.1.0"
edition = "2021"
[dependencies]
"#,
"rust_project"
);
std::fs::write(project_dir.join("Cargo.toml"), cargo_toml)
.context("Failed to write Cargo.toml")?;
Ok(())
}
pub fn read_cargo_toml(project_dir: &Path) -> anyhow::Result<String> {
let cargo_path = project_dir.join("Cargo.toml");
if !cargo_path.exists() {
return Err(anyhow::anyhow!("Cargo.toml not found"));
}
std::fs::read_to_string(&cargo_path)
.context(format!("Failed to read {}", cargo_path.display()))
}
pub fn write_cargo_toml_content(project_dir: &Path, content: &str) -> anyhow::Result<()> {
let cargo_path = project_dir.join("Cargo.toml");
std::fs::write(&cargo_path, content)
.context(format!("Failed to write {}", cargo_path.display()))?;
println!("{} {}", "Updated".green(), cargo_path.display());
Ok(())
}
pub fn update_workspace_members(project_dir: &Path) -> anyhow::Result<bool> {
let cargo_content = read_cargo_toml(project_dir)?;
let cargo_toml: toml::Value = toml::from_str(&cargo_content)
.context("Failed to parse Cargo.toml as valid TOML")?;
if cargo_toml.get("workspace").is_none() {
return Err(anyhow::anyhow!("Not a Cargo workspace (no [workspace] section in Cargo.toml)"));
}
let mut updated = false;
let mut existing_members = Vec::new();
if let Some(workspace) = cargo_toml.get("workspace").and_then(|w| w.as_table()) {
if let Some(members) = workspace.get("members").and_then(|m| m.as_array()) {
for member in members {
if let Some(member_str) = member.as_str() {
existing_members.push(member_str.to_string());
}
}
}
}
let mut crates_to_add = Vec::new();
for dir in &["client_old", "server", "ferrisup_common", "libs", "crates"] {
let dir_path = project_dir.join(dir);
if dir_path.exists() && dir_path.is_dir() {
let wildcard = format!("{}/* ", dir);
if !existing_members.contains(&wildcard) && !existing_members.iter().any(|m| m.starts_with(&format!("{}/", dir))) {
for entry in std::fs::read_dir(&dir_path).context(format!("Failed to read directory {}", dir_path.display()))? {
let entry = entry.context("Failed to read directory entry")?;
let path = entry.path();
if path.is_dir() && path.join("Cargo.toml").exists() {
let relative_path = format!("{}/{}", dir, path.file_name().unwrap().to_string_lossy());
if !existing_members.contains(&relative_path) {
crates_to_add.push(relative_path);
}
}
}
}
}
}
for entry in std::fs::read_dir(project_dir).context("Failed to read project directory")? {
let entry = entry.context("Failed to read directory entry")?;
let path = entry.path();
if path.is_dir() && path.join("Cargo.toml").exists() {
let dir_name = path.file_name().unwrap().to_string_lossy().to_string();
if ![
"src", "target", ".git", ".github", ".ferrisup"
].contains(&dir_name.as_str()) && !existing_members.contains(&dir_name) {
crates_to_add.push(dir_name);
}
}
}
if !crates_to_add.is_empty() {
updated = true;
let mut new_cargo = cargo_toml.clone();
let workspace = new_cargo.get_mut("workspace")
.and_then(|w| w.as_table_mut())
.expect("Workspace section should exist");
let members = if let Some(members) = workspace.get_mut("members").and_then(|m| m.as_array_mut()) {
members
} else {
workspace.insert("members".to_string(), toml::Value::Array(Vec::new()));
workspace.get_mut("members").and_then(|m| m.as_array_mut()).unwrap()
};
for crate_path in crates_to_add {
println!("Adding workspace member: {}", crate_path.green());
members.push(toml::Value::String(crate_path.to_string()));
}
let updated_content = toml::to_string(&new_cargo)
.context("Failed to serialize updated Cargo.toml")?;
write_cargo_toml_content(project_dir, &updated_content)?;
}
Ok(updated)
}
pub fn extract_dependencies(deps_table: &Item) -> anyhow::Result<Vec<(String, String, Option<Vec<String>>)>> {
let mut dependencies = Vec::new();
if let Some(deps_table) = deps_table.as_table() {
for (name, value) in deps_table.iter() {
if let Some(version) = value.as_str() {
dependencies.push((name.to_string(), version.to_string(), None));
} else if let Some(table) = value.as_table() {
if let Some(version) = table.get("version").and_then(|v| v.as_str()) {
let mut features = Vec::new();
if let Some(features_value) = table.get("features").and_then(|f| f.as_array()) {
for feature in features_value {
if let Some(feature_str) = feature.as_str() {
features.push(feature_str.to_string());
}
}
}
let features_option = if features.is_empty() { None } else { Some(features) };
dependencies.push((name.to_string(), version.to_string(), features_option));
}
}
}
}
Ok(dependencies)
}
pub fn update_cargo_with_dependencies(cargo_path: &Path, dependencies: Vec<(String, String, Option<Vec<String>>)>, dev: bool) -> anyhow::Result<()> {
let project_dir = cargo_path.parent().ok_or_else(|| anyhow!("Could not determine project directory"))?;
let current_dir = std::env::current_dir()?;
std::env::set_current_dir(project_dir)?;
for (name, version, features) in dependencies {
let mut cmd = std::process::Command::new("cargo");
cmd.arg("add").arg(&name);
if dev {
cmd.arg("--dev");
}
if version != "*" {
cmd.arg("--version").arg(&version);
}
if let Some(feat_list) = features {
if !feat_list.is_empty() {
let features_str = feat_list.join(",");
cmd.arg("--features").arg(features_str);
}
}
let output = cmd.output()
.context(format!("Failed to add dependency: {}", name))?;
if !output.status.success() {
println!("{} {}",
"Warning:".yellow().bold(),
format!("Failed to add dependency: {}", name).yellow());
if let Ok(err) = String::from_utf8(output.stderr) {
if !err.is_empty() {
println!("{}", err);
}
}
}
}
std::env::set_current_dir(current_dir)?;
Ok(())
}
pub fn is_crate_name_available(name: &str) -> Result<bool> {
let output = Command::new("cargo")
.arg("search")
.arg("--limit=1")
.arg(format!("^{}$", name)) .output()
.context("Failed to execute cargo search command")?;
let stdout = String::from_utf8_lossy(&output.stdout);
Ok(stdout.trim().is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
pub fn test_crate_availability() {
println!("Testing crate name availability checker...");
let existing_crate = "serde";
match is_crate_name_available(existing_crate) {
Ok(available) => println!("Crate '{}' availability: {}", existing_crate,
if available { "AVAILABLE ✅" } else { "NOT AVAILABLE ❌" }),
Err(e) => println!("Error checking '{}': {}", existing_crate, e),
}
let random_crate = format!("ferrisup-test-{}", std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs());
match is_crate_name_available(&random_crate) {
Ok(available) => println!("Crate '{}' availability: {}", random_crate,
if available { "AVAILABLE ✅" } else { "NOT AVAILABLE ❌" }),
Err(e) => println!("Error checking '{}': {}", random_crate, e),
}
let common_crate = "ferrisup_common";
match is_crate_name_available(common_crate) {
Ok(available) => println!("Crate '{}' availability: {}", common_crate,
if available { "AVAILABLE ✅" } else { "NOT AVAILABLE ❌" }),
Err(e) => println!("Error checking '{}': {}", common_crate, e),
}
}
#[test]
fn test_write_cargo_toml() -> anyhow::Result<()> {
let temp_dir = tempdir()?;
let project_dir = temp_dir.path().join("test_project");
fs::create_dir_all(&project_dir)?;
write_cargo_toml(&project_dir)?;
let cargo_path = project_dir.join("Cargo.toml");
assert!(cargo_path.exists());
let content = fs::read_to_string(cargo_path)?;
assert!(content.contains("name = \"rust_project\""));
Ok(())
}
#[test]
fn test_read_cargo_toml() -> anyhow::Result<()> {
let temp_dir = tempdir()?;
let test_dir = temp_dir.path().join("test_project");
fs::create_dir_all(&test_dir)?;
let cargo_content = r#"[package]
name = "test_project"
version = "0.1.0"
edition = "2021"
"#;
let cargo_path = test_dir.join("Cargo.toml");
fs::write(&cargo_path, cargo_content)?;
let read_content = read_cargo_toml(&test_dir)?;
assert_eq!(read_content, cargo_content);
Ok(())
}
#[test]
fn test_write_cargo_toml_content() -> anyhow::Result<()> {
let temp_dir = tempdir()?;
let test_dir = temp_dir.path().join("test_project");
fs::create_dir_all(&test_dir)?;
let content = r#"[package]
name = "test_project"
version = "0.1.0"
edition = "2021"
"#;
write_cargo_toml_content(&test_dir, content)?;
let cargo_path = test_dir.join("Cargo.toml");
assert!(cargo_path.exists());
let read_content = fs::read_to_string(cargo_path)?;
assert_eq!(read_content, content);
Ok(())
}
#[test]
fn test_update_workspace_members() -> anyhow::Result<()> {
let temp_dir = tempdir()?;
let workspace_dir = temp_dir.path().join("workspace");
fs::create_dir_all(&workspace_dir)?;
let cargo_content = r#"[workspace]
members = []
[workspace.package]
version = "0.1.0"
edition = "2021"
"#;
let cargo_path = workspace_dir.join("Cargo.toml");
fs::write(&cargo_path, cargo_content)?;
let component_dir = workspace_dir.join("component1");
fs::create_dir_all(&component_dir)?;
let component_cargo = r#"[package]
name = "component1"
version = "0.1.0"
edition = "2021"
"#;
fs::write(component_dir.join("Cargo.toml"), component_cargo)?;
let updated = update_workspace_members(&workspace_dir)?;
assert!(updated);
let updated_content = fs::read_to_string(cargo_path)?;
assert!(updated_content.contains("members = ["));
assert!(updated_content.contains("\"component1\""));
Ok(())
}
#[test]
fn test_extract_dependencies() -> anyhow::Result<()> {
let toml_str = r#"
[dependencies]
anyhow = "1.0"
"#;
let parsed: DocumentMut = toml_str.parse().unwrap();
let deps_table = &parsed["dependencies"];
let deps = extract_dependencies(deps_table)?;
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].0, "anyhow");
assert_eq!(deps[0].1, "1.0");
assert!(deps[0].2.is_none());
Ok(())
}
#[test]
fn test_update_cargo_with_dependencies() -> anyhow::Result<()> {
let temp_dir = tempdir()?;
let cargo_path = temp_dir.path().join("Cargo.toml");
let cargo_content = r#"[package]
name = "test-project"
version = "0.1.0"
edition = "2021"
[dependencies]
"#;
std::fs::write(&cargo_path, cargo_content)?;
assert!(cargo_path.exists());
Ok(())
}
#[test]
fn test_is_crate_name_available() -> anyhow::Result<()> {
let result = is_crate_name_available("serde")?;
assert!(!result, "'serde' should not be available");
let random_name = format!("ferrisup-test-{}", std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)?
.as_secs());
let result = is_crate_name_available(&random_name)?;
assert!(result, "Random crate name should be available");
let result = is_crate_name_available("ferrisup")?;
assert!(!result, "'ferrisup' should not be available");
Ok(())
}
}