project.rs
1 use crate::error::{GramrError, Result}; 2 use crate::language::Language; 3 use std::fs; 4 use std::path::PathBuf; 5 use std::process::Command; 6 7 pub trait Project { 8 fn ensure_directories(&self) -> Result<()>; 9 fn src_dir(&self) -> PathBuf; 10 fn test_dir(&self) -> PathBuf; 11 fn script_dir(&self) -> PathBuf; 12 fn has_openzeppelin(&self) -> bool; 13 fn install_openzeppelin(&self) -> Result<()>; 14 fn has_openzeppelin_upgradeable(&self) -> bool; 15 fn install_openzeppelin_upgradeable(&self) -> Result<()>; 16 } 17 18 pub enum ProjectType { 19 Foundry(crate::foundry::FoundryProject), 20 Cargo(CargoProject), 21 } 22 23 impl Project for ProjectType { 24 fn ensure_directories(&self) -> Result<()> { 25 match self { 26 ProjectType::Foundry(p) => p.ensure_directories(), 27 ProjectType::Cargo(p) => p.ensure_directories(), 28 } 29 } 30 31 fn src_dir(&self) -> PathBuf { 32 match self { 33 ProjectType::Foundry(p) => p.src_dir(), 34 ProjectType::Cargo(p) => p.src_dir(), 35 } 36 } 37 38 fn test_dir(&self) -> PathBuf { 39 match self { 40 ProjectType::Foundry(p) => p.test_dir(), 41 ProjectType::Cargo(p) => p.test_dir(), 42 } 43 } 44 45 fn script_dir(&self) -> PathBuf { 46 match self { 47 ProjectType::Foundry(p) => p.script_dir(), 48 ProjectType::Cargo(p) => p.script_dir(), 49 } 50 } 51 52 fn has_openzeppelin(&self) -> bool { 53 match self { 54 ProjectType::Foundry(p) => p.has_openzeppelin(), 55 ProjectType::Cargo(p) => p.has_openzeppelin(), 56 } 57 } 58 59 fn install_openzeppelin(&self) -> Result<()> { 60 match self { 61 ProjectType::Foundry(p) => p.install_openzeppelin(), 62 ProjectType::Cargo(p) => p.install_openzeppelin(), 63 } 64 } 65 66 fn has_openzeppelin_upgradeable(&self) -> bool { 67 match self { 68 ProjectType::Foundry(p) => p.has_openzeppelin_upgradeable(), 69 ProjectType::Cargo(p) => p.has_openzeppelin_upgradeable(), 70 } 71 } 72 73 fn install_openzeppelin_upgradeable(&self) -> Result<()> { 74 match self { 75 ProjectType::Foundry(p) => p.install_openzeppelin_upgradeable(), 76 ProjectType::Cargo(p) => p.install_openzeppelin_upgradeable(), 77 } 78 } 79 } 80 81 impl ProjectType { 82 pub fn detect(language: &Language) -> Result<Self> { 83 match language { 84 Language::Solidity => { 85 let project = crate::foundry::FoundryProject::detect()?; 86 Ok(ProjectType::Foundry(project)) 87 } 88 Language::RustStylus => { 89 let project = CargoProject::detect()?; 90 Ok(ProjectType::Cargo(project)) 91 } 92 } 93 } 94 } 95 96 pub struct CargoProject { 97 root: PathBuf, 98 } 99 100 impl CargoProject { 101 pub fn detect() -> Result<Self> { 102 let current_dir = std::env::current_dir() 103 .map_err(|e| GramrError::Other(format!("Failed to get current directory: {}", e)))?; 104 105 let cargo_toml = current_dir.join("Cargo.toml"); 106 if !cargo_toml.exists() { 107 return Err(GramrError::ProjectNotFound( 108 "No Cargo.toml found. Please run from a Rust project directory.".to_string(), 109 )); 110 } 111 112 Ok(Self { root: current_dir }) 113 } 114 115 fn cargo_toml_path(&self) -> PathBuf { 116 self.root.join("Cargo.toml") 117 } 118 } 119 120 impl Project for CargoProject { 121 fn ensure_directories(&self) -> Result<()> { 122 let src = self.src_dir(); 123 if !src.exists() { 124 fs::create_dir_all(&src) 125 .map_err(|e| GramrError::Other(format!("Failed to create src directory: {}", e)))?; 126 } 127 128 let tests = self.test_dir(); 129 if !tests.exists() { 130 fs::create_dir_all(&tests).map_err(|e| { 131 GramrError::Other(format!("Failed to create tests directory: {}", e)) 132 })?; 133 } 134 135 Ok(()) 136 } 137 138 fn src_dir(&self) -> PathBuf { 139 self.root.join("src") 140 } 141 142 fn test_dir(&self) -> PathBuf { 143 self.root.join("tests") 144 } 145 146 fn script_dir(&self) -> PathBuf { 147 // Stylus projects don't have a script directory like Foundry 148 self.root.join("scripts") 149 } 150 151 fn has_openzeppelin(&self) -> bool { 152 // Check if openzeppelin-stylus is in Cargo.toml 153 if let Ok(content) = fs::read_to_string(self.cargo_toml_path()) { 154 content.contains("openzeppelin-stylus") 155 } else { 156 false 157 } 158 } 159 160 fn install_openzeppelin(&self) -> Result<()> { 161 let output = Command::new("cargo") 162 .args(&["add", "openzeppelin-stylus@=0.3.0"]) 163 .current_dir(&self.root) 164 .output() 165 .map_err(|e| GramrError::Other(format!("Failed to run cargo add: {}", e)))?; 166 167 if !output.status.success() { 168 let stderr = String::from_utf8_lossy(&output.stderr); 169 return Err(GramrError::Other(format!( 170 "Failed to add openzeppelin-stylus: {}", 171 stderr 172 ))); 173 } 174 175 Ok(()) 176 } 177 178 fn has_openzeppelin_upgradeable(&self) -> bool { 179 // For Stylus, upgradeable contracts are part of the same package 180 self.has_openzeppelin() 181 } 182 183 fn install_openzeppelin_upgradeable(&self) -> Result<()> { 184 // Upgradeable contracts are not yet available for OpenZeppelin Stylus 185 Err(GramrError::Other( 186 "Upgradeable contracts are not yet supported for Rust/Stylus projects".to_string(), 187 )) 188 } 189 } 190 191 #[cfg(test)] 192 mod tests { 193 use super::*; 194 use std::fs; 195 use tempfile::TempDir; 196 197 fn create_test_cargo_project() -> (TempDir, CargoProject) { 198 let temp_dir = TempDir::new().unwrap(); 199 let project_path = temp_dir.path().to_path_buf(); 200 201 // Create Cargo.toml 202 fs::write( 203 project_path.join("Cargo.toml"), 204 r#"[package] 205 name = "test-project" 206 version = "0.0.1" 207 edition = "2021" 208 209 [dependencies] 210 "#, 211 ) 212 .unwrap(); 213 214 // Create basic directory structure 215 fs::create_dir_all(project_path.join("src")).unwrap(); 216 217 let project = CargoProject { root: project_path }; 218 (temp_dir, project) 219 } 220 221 fn create_test_cargo_project_with_oz() -> (TempDir, CargoProject) { 222 let temp_dir = TempDir::new().unwrap(); 223 let project_path = temp_dir.path().to_path_buf(); 224 225 // Create Cargo.toml with OpenZeppelin dependency 226 fs::write( 227 project_path.join("Cargo.toml"), 228 r#"[package] 229 name = "test-project" 230 version = "0.0.1" 231 edition = "2021" 232 233 [dependencies] 234 openzeppelin-stylus = "0.3.0" 235 "#, 236 ) 237 .unwrap(); 238 239 fs::create_dir_all(project_path.join("src")).unwrap(); 240 241 let project = CargoProject { root: project_path }; 242 (temp_dir, project) 243 } 244 245 fn create_test_foundry_project() -> (TempDir, crate::foundry::FoundryProject) { 246 let temp_dir = TempDir::new().unwrap(); 247 let project_path = temp_dir.path().to_path_buf(); 248 249 // Create foundry.toml 250 fs::write( 251 project_path.join("foundry.toml"), 252 "[profile.default]\nsrc = \"src\"\ntest = \"test\"\nscript = \"script\"\n", 253 ) 254 .unwrap(); 255 256 // Create directories 257 fs::create_dir_all(project_path.join("src")).unwrap(); 258 fs::create_dir_all(project_path.join("test")).unwrap(); 259 fs::create_dir_all(project_path.join("script")).unwrap(); 260 fs::create_dir_all(project_path.join("lib")).unwrap(); 261 262 let project = crate::foundry::FoundryProject { 263 root: project_path.clone(), 264 src_dir: project_path.join("src"), 265 test_dir: project_path.join("test"), 266 script_dir: project_path.join("script"), 267 }; 268 (temp_dir, project) 269 } 270 271 #[test] 272 fn test_cargo_project_detect_success() { 273 let (_temp_dir, project) = create_test_cargo_project(); 274 275 // Change to the project directory for detection 276 let original_dir = std::env::current_dir().unwrap(); 277 std::env::set_current_dir(&project.root).unwrap(); 278 279 let result = CargoProject::detect(); 280 281 // Restore original directory 282 std::env::set_current_dir(original_dir).unwrap(); 283 284 assert!(result.is_ok()); 285 let detected_project = result.unwrap(); 286 assert_eq!(detected_project.root, project.root); 287 } 288 289 #[test] 290 fn test_cargo_project_detect_no_cargo_toml() { 291 let temp_dir = TempDir::new().unwrap(); 292 let project_path = temp_dir.path().to_path_buf(); 293 294 let original_dir = std::env::current_dir().unwrap(); 295 std::env::set_current_dir(&project_path).unwrap(); 296 297 let result = CargoProject::detect(); 298 299 std::env::set_current_dir(original_dir).unwrap(); 300 301 assert!(result.is_err()); 302 if let Err(GramrError::ProjectNotFound(msg)) = result { 303 assert!(msg.contains("No Cargo.toml found")); 304 } else { 305 panic!("Expected ProjectNotFound error"); 306 } 307 } 308 309 #[test] 310 fn test_cargo_project_ensure_directories() { 311 let (_temp_dir, project) = create_test_cargo_project(); 312 313 assert!(project.ensure_directories().is_ok()); 314 315 assert!(project.src_dir().exists()); 316 assert!(project.test_dir().exists()); 317 } 318 319 #[test] 320 fn test_cargo_project_directory_paths() { 321 let (_temp_dir, project) = create_test_cargo_project(); 322 323 assert_eq!(project.src_dir(), project.root.join("src")); 324 assert_eq!(project.test_dir(), project.root.join("tests")); 325 assert_eq!(project.script_dir(), project.root.join("scripts")); 326 assert_eq!(project.cargo_toml_path(), project.root.join("Cargo.toml")); 327 } 328 329 #[test] 330 fn test_cargo_project_has_openzeppelin_false() { 331 let (_temp_dir, project) = create_test_cargo_project(); 332 333 assert!(!project.has_openzeppelin()); 334 assert!(!project.has_openzeppelin_upgradeable()); 335 } 336 337 #[test] 338 fn test_cargo_project_has_openzeppelin_true() { 339 let (_temp_dir, project) = create_test_cargo_project_with_oz(); 340 341 assert!(project.has_openzeppelin()); 342 assert!(project.has_openzeppelin_upgradeable()); // Same as has_openzeppelin for Stylus 343 } 344 345 #[test] 346 fn test_cargo_project_install_openzeppelin_upgradeable_not_supported() { 347 let (_temp_dir, project) = create_test_cargo_project(); 348 349 let result = project.install_openzeppelin_upgradeable(); 350 assert!(result.is_err()); 351 if let Err(GramrError::Other(msg)) = result { 352 assert!(msg.contains("not yet supported for Rust/Stylus projects")); 353 } else { 354 panic!("Expected Other error"); 355 } 356 } 357 358 #[test] 359 fn test_project_type_foundry() { 360 let (_temp_dir, foundry_project) = create_test_foundry_project(); 361 let project_type = ProjectType::Foundry(foundry_project); 362 363 // Test Project trait implementation 364 assert!(project_type.ensure_directories().is_ok()); 365 assert!(project_type.src_dir().ends_with("src")); 366 assert!(project_type.test_dir().ends_with("test")); 367 assert!(project_type.script_dir().ends_with("script")); 368 assert!(!project_type.has_openzeppelin()); 369 assert!(!project_type.has_openzeppelin_upgradeable()); 370 } 371 372 #[test] 373 fn test_project_type_cargo() { 374 let (_temp_dir, cargo_project) = create_test_cargo_project(); 375 let project_type = ProjectType::Cargo(cargo_project); 376 377 // Test Project trait implementation 378 assert!(project_type.ensure_directories().is_ok()); 379 assert!(project_type.src_dir().ends_with("src")); 380 assert!(project_type.test_dir().ends_with("tests")); 381 assert!(project_type.script_dir().ends_with("scripts")); 382 assert!(!project_type.has_openzeppelin()); 383 assert!(!project_type.has_openzeppelin_upgradeable()); 384 } 385 386 #[test] 387 fn test_project_type_cargo_with_openzeppelin() { 388 let (_temp_dir, cargo_project) = create_test_cargo_project_with_oz(); 389 let project_type = ProjectType::Cargo(cargo_project); 390 391 assert!(project_type.has_openzeppelin()); 392 assert!(project_type.has_openzeppelin_upgradeable()); 393 394 let result = project_type.install_openzeppelin_upgradeable(); 395 assert!(result.is_err()); 396 } 397 398 #[test] 399 fn test_project_trait_methods() { 400 let (_temp_dir, cargo_project) = create_test_cargo_project(); 401 402 // Test direct trait methods 403 let project: &dyn Project = &cargo_project; 404 405 assert!(project.ensure_directories().is_ok()); 406 assert!(project.src_dir().ends_with("src")); 407 assert!(project.test_dir().ends_with("tests")); 408 assert!(project.script_dir().ends_with("scripts")); 409 assert!(!project.has_openzeppelin()); 410 assert!(!project.has_openzeppelin_upgradeable()); 411 } 412 413 #[test] 414 fn test_cargo_project_detect_missing_current_dir() { 415 // This test would be hard to simulate without mocking, but we can at least 416 // ensure the error handling path exists by checking the code structure 417 let (_temp_dir, _project) = create_test_cargo_project(); 418 419 // The actual test for missing current_dir would require complex setup 420 // but the error handling is covered in the detect() implementation 421 assert!(true); // Placeholder for complex environment test 422 } 423 424 #[test] 425 fn test_cargo_project_has_openzeppelin_with_malformed_toml() { 426 let temp_dir = TempDir::new().unwrap(); 427 let project_path = temp_dir.path().to_path_buf(); 428 429 // Create malformed Cargo.toml (but still readable) 430 fs::write( 431 project_path.join("Cargo.toml"), 432 "invalid toml content but contains openzeppelin-stylus somewhere", 433 ) 434 .unwrap(); 435 436 let project = CargoProject { root: project_path }; 437 438 // Should still detect the string even in malformed toml 439 assert!(project.has_openzeppelin()); 440 } 441 442 #[test] 443 fn test_cargo_project_has_openzeppelin_with_unreadable_toml() { 444 let temp_dir = TempDir::new().unwrap(); 445 let project_path = temp_dir.path().to_path_buf(); 446 447 // Create project without Cargo.toml 448 let project = CargoProject { root: project_path }; 449 450 // Should return false when Cargo.toml doesn't exist or can't be read 451 assert!(!project.has_openzeppelin()); 452 } 453 454 #[test] 455 fn test_cargo_project_install_openzeppelin_success_simulation() { 456 let (_temp_dir, project) = create_test_cargo_project(); 457 458 // We can't easily test the actual cargo command without a real cargo installation 459 // But we can test the error path exists and is handled properly 460 // In a real integration test, this would actually run cargo add 461 462 // For now, just verify the project structure is set up correctly for the command 463 assert!(project.cargo_toml_path().exists()); 464 assert_eq!(project.cargo_toml_path().file_name().unwrap(), "Cargo.toml"); 465 } 466 467 #[test] 468 fn test_project_type_detect_would_call_correct_methods() { 469 // This test verifies the dispatch logic without actually calling detect 470 // since detect requires specific environment setup 471 472 // Test the match logic exists for both language types 473 use crate::language::Language; 474 475 // We can't easily test detect() without proper environment setup, 476 // but we can verify the enum structure and methods exist 477 match Language::Solidity { 478 Language::Solidity => { 479 // Would call FoundryProject::detect() 480 assert!(true); 481 } 482 Language::RustStylus => { 483 // Would call CargoProject::detect() 484 assert!(true); 485 } 486 } 487 488 match Language::RustStylus { 489 Language::Solidity => { 490 assert!(false, "Wrong match"); 491 } 492 Language::RustStylus => { 493 assert!(true); 494 } 495 } 496 } 497 }