/ lib / src / project.rs
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  }