1crate::ix!();
3
4#[derive(Builder,Getters,Debug,Clone)]
5#[getset(get="pub")]
6#[builder(setter(into))]
7pub struct CrateHandle {
8 crate_path: PathBuf,
9 cargo_toml_handle: Arc<AsyncMutex<CargoToml>>,
10}
11
12impl Named for CrateHandle {
13 fn name(&self) -> std::borrow::Cow<'_, str> {
14 use tracing::{trace, debug, info};
15
16 trace!("Entering CrateHandle::name");
17
18 let cargo_toml_handle = self.cargo_toml_handle.clone();
20
21 let package_name = sync_run_async(async move {
22 trace!("Locking cargo_toml_handle in async block");
23 let guard = cargo_toml_handle.lock().await;
24 let name = guard
25 .package_name()
26 .expect("Expected a valid package name");
27 debug!("Retrieved package name from guard: {:?}", name);
28 name
29 });
30
31 info!("Synchronous retrieval of package name succeeded: {}", package_name);
32
33 let trimmed = package_name.trim_matches('"').to_string();
34 trace!("Trimmed package name: {}", trimmed);
35
36 std::borrow::Cow::Owned(trimmed)
37 }
38}
39
40impl Versioned for CrateHandle {
41 type Error = CrateError;
42
43 fn version(&self) -> Result<semver::Version, Self::Error> {
44 trace!("CrateHandle::version called");
45
46 let cargo_toml_arc = self.cargo_toml();
48
49 let guard = match cargo_toml_arc.try_lock() {
52 Ok(g) => g,
53 Err(_) => {
54 error!("CrateHandle::version: cannot lock cargo_toml right now => returning a general error");
55 return Err(CrateError::CargoTomlIsLocked);
56 }
57 };
58
59 match guard.version() {
61 Ok(ver) => {
62 info!("CrateHandle: returning semver={}", ver);
63 Ok(ver)
64 }
65 Err(e) => {
66 error!("CrateHandle: cargo_toml.version() => encountered error: {:?}", e);
67 match e {
69 CargoTomlError::FileNotFound { missing_file } => {
70 Err(CrateError::IoError {
72 io_error: Arc::new(std::io::Error::new(
73 std::io::ErrorKind::NotFound,
74 format!("Cargo.toml missing at {}", missing_file.display()),
75 )),
76 context: format!("CrateHandle::version => no Cargo.toml at {:?}", missing_file),
77 })
78 }
79 other => {
80 Err(CrateError::CargoTomlError(other))
82 }
83 }
84 }
85 }
86 }
87}
88
89impl<P> CrateHandleInterface<P> for CrateHandle
90where
91 for<'async_trait>
92 P
93 : HasCargoTomlPathBuf
94 + HasCargoTomlPathBufSync
95 + AsRef<Path>
96 + Send
97 + Sync
98 + 'async_trait,
99
100 CrateError
101 : From<<P as HasCargoTomlPathBuf>::Error>
102 + From<<P as HasCargoTomlPathBufSync>::Error>,
103{}
104
105#[async_trait]
106impl<P> AsyncTryFrom<P> for CrateHandle
107where
108 for<'async_trait>
109 P
110 : HasCargoTomlPathBuf
111 + HasCargoTomlPathBufSync
112 + AsRef<Path>
113 + Send
114 + Sync
115 + 'async_trait,
116
117 CrateError
118 : From<<P as HasCargoTomlPathBuf>::Error>
119 + From<<P as HasCargoTomlPathBufSync>::Error>,
120{
121 type Error = CrateError;
122
123 async fn new(crate_path: &P) -> Result<Self,Self::Error> {
125
126 let cargo_toml_path = crate_path.cargo_toml_path_buf().await?;
127
128 let cargo_toml_handle = Arc::new(AsyncMutex::new(CargoToml::new(cargo_toml_path).await?));
129
130 Ok(Self {
131 cargo_toml_handle,
132 crate_path: crate_path.as_ref().to_path_buf(),
133 })
134 }
135}
136
137impl CrateHandle
138{
139 pub fn new_sync<P>(crate_path: &P) -> Result<Self,CrateError>
141 where
142 for<'async_trait>
143 P
144 : HasCargoTomlPathBuf
145 + HasCargoTomlPathBufSync
146 + AsRef<Path>
147 + Send
148 + Sync
149 + 'async_trait,
150
151 CrateError
152 : From<<P as HasCargoTomlPathBuf>::Error>
153 + From<<P as HasCargoTomlPathBufSync>::Error>
154 {
155
156 let cargo_toml_path = crate_path.cargo_toml_path_buf_sync()?;
157
158 let cargo_toml_handle = Arc::new(AsyncMutex::new(CargoToml::new_sync(cargo_toml_path)?));
159
160 Ok(Self {
161 cargo_toml_handle,
162 crate_path: crate_path.as_ref().to_path_buf(),
163 })
164 }
165}
166
167#[async_trait]
168impl ValidateIntegrity for CrateHandle {
169 type Error = CrateError;
170
171 async fn validate_integrity(&self) -> Result<(), Self::Error> {
172 trace!("CrateHandle::validate_integrity() - forcing a re-parse from disk to catch sabotage or missing fields.");
173
174 let cargo_toml_arc = self.cargo_toml();
176 let ct_guard = cargo_toml_arc.lock().await;
177 match ct_guard.version() {
178 Err(e) => {
179 error!("CrateHandle: cargo_toml.version() => encountered error: {:?}", e);
180 match e {
182 CargoTomlError::FileNotFound { missing_file } => {
183 return Err(CrateError::IoError {
184 io_error: Arc::new(std::io::Error::new(
185 std::io::ErrorKind::NotFound,
186 format!("Cargo.toml missing at {}", missing_file.display()),
187 )),
188 context: format!("validate_integrity: no Cargo.toml at {:?}", missing_file),
189 });
190 }
191 _ => {
192 return Err(CrateError::CargoTomlError(e));
193 }
194 }
195 }
196 Ok(ver) => {
197 trace!("CrateHandle::validate_integrity => version is valid ({})", ver);
198 }
199 }
200
201 self.check_src_directory_contains_valid_files()?;
203
204 self.check_readme_exists()?;
206
207 info!("CrateHandle::validate_integrity passed successfully");
208 Ok(())
209 }
210}
211
212impl CheckIfSrcDirectoryContainsValidFiles for CrateHandle {
213
214 fn check_src_directory_contains_valid_files(&self) -> Result<(), CrateError> {
216 let src_dir = self.as_ref().join("src");
217 let main_rs = src_dir.join("main.rs");
218 let lib_rs = src_dir.join("lib.rs");
219
220 if !main_rs.exists() && !lib_rs.exists() {
221 return Err(CrateError::FileNotFound {
222 missing_file: src_dir.join("main.rs or lib.rs"),
223 });
224 }
225
226 Ok(())
228 }
229}
230
231impl CheckIfReadmeExists for CrateHandle {
232
233 fn check_readme_exists(&self) -> Result<(), CrateError> {
235 let readme_path = self.as_ref().join("README.md");
236 if !readme_path.exists() {
237 return Err(CrateError::FileNotFound {
238 missing_file: readme_path,
239 });
240 }
241 Ok(())
242 }
243}
244
245#[async_trait]
246impl GetReadmePath for CrateHandle {
247
248 async fn readme_path(&self) -> Result<Option<PathBuf>, CrateError> {
250 let readme_path = self.crate_path.join("README.md");
251 if fs::metadata(&readme_path).await.is_ok() {
252 Ok(Some(readme_path))
253 } else {
254 Ok(None)
255 }
256 }
257}
258
259#[async_trait]
260impl GetSourceFilesWithExclusions for CrateHandle {
261
262 async fn source_files_excluding(&self, exclude_files: &[&str]) -> Result<Vec<PathBuf>, CrateError> {
264 self.get_files_in_dir_with_exclusions("src", "rs", exclude_files).await
265 }
266}
267
268#[async_trait]
269impl GetTestFiles for CrateHandle {
270
271 async fn test_files(&self) -> Result<Vec<PathBuf>, CrateError> {
273 self.get_files_in_dir("tests", "rs").await
274 }
275}
276
277impl HasTestsDirectory for CrateHandle {
278
279 fn has_tests_directory(&self) -> bool {
280 self.crate_path.join("tests").exists()
281 }
282}
283
284#[async_trait]
285impl GetFilesInDirectory for CrateHandle {
286
287 async fn get_files_in_dir(&self, dir_name: &str, extension: &str) -> Result<Vec<PathBuf>, CrateError> {
289 self.get_files_in_dir_with_exclusions(dir_name, extension, &[]).await
290 }
291}
292
293#[async_trait]
294impl GetFilesInDirectoryWithExclusions for CrateHandle {
295
296 async fn get_files_in_dir_with_exclusions(
299 &self,
300 dir_name: &str,
301 extension: &str,
302 exclude_files: &[&str]
303 ) -> Result<Vec<PathBuf>, CrateError> {
304 let dir_path = self.crate_path.join(dir_name);
305
306 if !fs::metadata(&dir_path).await.is_ok() {
307 return Err(CrateError::DirectoryNotFound {
308 missing_directory: dir_path,
309 });
310 }
311
312 let mut files = vec![];
313
314 let mut entries = fs::read_dir(dir_path)
315 .await
316 .map_err(|e| DirectoryError::ReadDirError {io: e.into() })?;
317
318 while let Some(entry)
319 = entries.next_entry()
320 .await
321 .map_err(|e| DirectoryError::GetNextEntryError {io: e.into() })?
322 {
323 let path = entry.path();
324 let file_name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| {
325 CrateError::FailedToGetFileNameForPath {
326 path: path.to_path_buf()
327 }
328 })?;
329
330 if path.extension().and_then(|e| e.to_str()) == Some(extension) && !exclude_files.contains(&file_name) {
331 files.push(path);
332 }
333 }
334
335 Ok(files)
336 }
337}
338
339impl HasCargoToml for CrateHandle {
340
341 fn cargo_toml(&self) -> Arc<AsyncMutex<dyn CargoTomlInterface>> {
342 self.cargo_toml_handle.clone()
343 }
344}
345
346impl CrateHandle {
347
348 pub fn cargo_toml_direct(&self) -> Arc<AsyncMutex<CargoToml>> {
350 self.cargo_toml_handle.clone()
351 }
352}
353
354
355impl AsRef<Path> for CrateHandle {
356 fn as_ref(&self) -> &Path {
358 &self.crate_path
359 }
360}
361
362#[async_trait]
364impl GetInternalDependencies for CrateHandle {
365 async fn internal_dependencies(&self) -> Result<Vec<String>, CrateError> {
366 let cargo_arc = self.cargo_toml();
368 let cargo_guard = cargo_arc.lock().await;
369
370 let mut results = Vec::new();
376
377 let empty = toml::value::Table::new();
378
379 let root_table = cargo_guard.get_content()
380 .as_table()
381 .unwrap_or_else(|| {
382 &empty
386 });
387
388 for deps_key in &["dependencies", "dev-dependencies", "build-dependencies"] {
390 if let Some(deps_val) = root_table.get(*deps_key).and_then(|v| v.as_table()) {
391 for (dep_name, dep_item) in deps_val.iter() {
393 if let Some(dep_tbl) = dep_item.as_table() {
395 if dep_tbl.get("path").is_some() {
396 results.push(dep_name.to_string());
398 }
399 }
400 else if dep_item.is_str() {
401 }
403 }
405 }
406 }
407
408 Ok(results)
409 }
410}
411
412#[cfg(test)]
413mod test_crate_handle {
414 use super::*;
415 use std::path::{Path, PathBuf};
416 use tempfile::{tempdir, TempDir};
417 use tokio::fs::{File, create_dir_all};
418 use tokio::io::AsyncWriteExt;
419
420 async fn write_file(file_path: &Path, content: &str) {
422 if let Some(parent_dir) = file_path.parent() {
423 create_dir_all(parent_dir)
424 .await
425 .expect("Failed to create parent directories");
426 }
427 let mut f = File::create(file_path)
428 .await
429 .unwrap_or_else(|e| panic!("Could not create file {}: {e}", file_path.display()));
430 f.write_all(content.as_bytes())
431 .await
432 .unwrap_or_else(|e| panic!("Failed to write to file {}: {e}", file_path.display()));
433 }
434
435 fn minimal_cargo_toml(name: &str, version: &str) -> String {
438 format!(
439 r#"[package]
440name = "{name}"
441version = "{version}"
442authors = ["Some Body"]
443license = "MIT"
444"#,
445 )
446 }
447
448 async fn create_crate_handle_in_temp(
452 crate_name: &str,
453 crate_version: &str,
454 create_src_dir: bool,
455 create_tests_dir: bool,
456 create_readme: bool,
457 main_or_lib: Option<&str>, ) -> (TempDir, CrateHandle) {
459 let tmp_dir = tempdir().expect("Failed to create temp dir");
460 let root_path = tmp_dir.path().to_path_buf();
461
462 let cargo_toml_content = minimal_cargo_toml(crate_name, crate_version);
464 let cargo_toml_path = root_path.join("Cargo.toml");
465 write_file(&cargo_toml_path, &cargo_toml_content).await;
466
467 if create_src_dir {
469 if let Some(which) = main_or_lib {
470 let file_name = format!("{which}.rs");
471 let file_path = root_path.join("src").join(file_name);
472 write_file(&file_path, "// sample content").await;
473 }
474 }
475
476 if create_tests_dir {
478 let test_file_path = root_path.join("tests").join("test_basic.rs");
479 write_file(&test_file_path, "// test file content").await;
480 }
481
482 if create_readme {
484 let readme_path = root_path.join("README.md");
485 write_file(&readme_path, "# My Crate\nSome description.").await;
486 }
487
488 #[derive(Clone)]
490 struct TempCratePath(PathBuf);
491
492 impl AsRef<Path> for TempCratePath {
493 fn as_ref(&self) -> &Path {
494 self.0.as_ref()
495 }
496 }
497
498 let temp_crate_path = TempCratePath(root_path.clone());
500
501 let handle = CrateHandle::new(&temp_crate_path)
503 .await
504 .expect("Failed to create CrateHandle from temp directory");
505
506 (tmp_dir, handle)
507 }
508
509 #[tokio::test]
515 async fn test_name_and_version() {
516 let (_tmp_dir, handle) =
517 create_crate_handle_in_temp("test_crate", "0.1.0", false, false, false, None).await;
518 eprintln!("handle: {:#?}", handle);
519 assert_eq!(handle.name(), "test_crate");
520 eprintln!("handle.name(): {:#?}", handle.name());
521 let ver = handle.version().expect("Expected valid version");
522 eprintln!("handle.version(): {:#?}", handle.version());
523 assert_eq!(ver.to_string(), "0.1.0");
524 }
525
526 #[tokio::test]
528 async fn test_check_src_directory_contains_valid_files_main_rs() {
529 let (_tmp_dir, handle) = create_crate_handle_in_temp(
530 "mycrate",
531 "0.1.0",
532 true, false, false, Some("main"),
536 )
537 .await;
538
539 handle.check_src_directory_contains_valid_files().expect("Should find main.rs");
541 }
542
543 #[tokio::test]
545 async fn test_check_src_directory_contains_valid_files_missing_main_and_lib() {
546 let (_tmp_dir, handle) = create_crate_handle_in_temp(
547 "mycrate",
548 "0.1.0",
549 true, false, false, None, )
554 .await;
555
556 let result = handle.check_src_directory_contains_valid_files();
557 assert!(
558 result.is_err(),
559 "Expected an error because neither main.rs nor lib.rs is present"
560 );
561 match result {
562 Err(CrateError::FileNotFound { missing_file }) => {
563 let missing = missing_file.to_string_lossy();
564 assert!(
565 missing.contains("main.rs or lib.rs"),
566 "Error message should mention main.rs or lib.rs"
567 );
568 }
569 _ => panic!("Expected CrateError::FileNotFound with mention of main.rs or lib.rs"),
570 }
571 }
572
573 #[tokio::test]
575 async fn test_check_readme_exists_ok() {
576 let (_tmp_dir, handle) = create_crate_handle_in_temp(
577 "mycrate",
578 "0.1.0",
579 true, false, true, Some("lib"),
583 )
584 .await;
585
586 handle.check_readme_exists().expect("README.md should exist");
588 }
589
590 #[tokio::test]
592 async fn test_check_readme_exists_missing() {
593 let (_tmp_dir, handle) = create_crate_handle_in_temp(
594 "mycrate",
595 "0.1.0",
596 true, false, false, Some("lib"),
600 )
601 .await;
602
603 let result = handle.check_readme_exists();
604 assert!(result.is_err());
605 match result {
606 Err(CrateError::FileNotFound { missing_file }) => {
607 let missing = missing_file.to_string_lossy();
608 assert!(
609 missing.contains("README.md"),
610 "Expected error referencing README.md"
611 );
612 }
613 _ => panic!("Expected CrateError::FileNotFound for missing README.md"),
614 }
615 }
616
617 #[tokio::test]
619 async fn test_has_tests_directory() {
620 let (_tmp_dir, handle_no_tests) = create_crate_handle_in_temp(
621 "mycrate",
622 "0.1.0",
623 true,
624 false, false,
626 Some("lib"),
627 )
628 .await;
629 assert!(
630 !handle_no_tests.has_tests_directory(),
631 "Expected false, no tests/ folder"
632 );
633
634 let (_tmp_dir, handle_with_tests) = create_crate_handle_in_temp(
635 "mycrate",
636 "0.1.0",
637 true,
638 true, false,
640 Some("lib"),
641 )
642 .await;
643 assert!(
644 handle_with_tests.has_tests_directory(),
645 "Expected true, tests/ folder created"
646 );
647 }
648
649 #[tokio::test]
651 async fn test_file_enumeration_in_source_and_tests() {
652 let (_tmp_dir, handle) = create_crate_handle_in_temp(
653 "mycrate",
654 "0.1.0",
655 true, true, true, Some("lib"),
659 )
660 .await;
661
662 let extra_src = handle.as_ref().join("src").join("extra.rs");
664 write_file(&extra_src, "// extra file").await;
665
666 let extra_test = handle.as_ref().join("tests").join("extra_test.rs");
668 write_file(&extra_test, "// extra test file").await;
669
670 let src_files = handle
672 .source_files_excluding(&[])
673 .await
674 .expect("Should list src files");
675 assert_eq!(src_files.len(), 2, "Should find 2 .rs files in src");
677
678 let test_files = handle.test_files().await.expect("Should list test files");
680 assert_eq!(test_files.len(), 2, "Should find 2 .rs files in tests");
682 }
683
684 #[tokio::test]
686 async fn test_source_files_excluding() {
687 let (_tmp_dir, handle) = create_crate_handle_in_temp(
688 "excluded_crate",
689 "0.1.0",
690 true,
691 false,
692 true,
693 Some("lib"),
694 )
695 .await;
696
697 let extra_src = handle.as_ref().join("src").join("exclude_me.rs");
699 write_file(&extra_src, "// exclude me").await;
700
701 let src_files = handle
703 .source_files_excluding(&["exclude_me.rs"])
704 .await
705 .unwrap();
706 assert_eq!(src_files.len(), 1, "Expected to exclude exclude_me.rs");
707 let only_file_name = src_files[0]
708 .file_name()
709 .unwrap()
710 .to_string_lossy()
711 .into_owned();
712 assert_eq!(only_file_name, "lib.rs");
713 }
714
715 #[traced_test]
718 async fn test_validate_integrity() {
719 let (_tmp_dir, handle_ok) = create_crate_handle_in_temp(
721 "integrity_crate",
722 "0.1.1",
723 true,
724 false,
725 true,
726 Some("lib"),
727 )
728 .await;
729 let res_ok = handle_ok.validate_integrity().await;
730 assert!(res_ok.is_ok(), "Expected valid integrity with a src file and README");
731
732 let (_tmp_dir, handle_bad_src) = create_crate_handle_in_temp(
734 "bad_src_crate",
735 "0.1.0",
736 true,
737 false,
738 true,
739 None, )
741 .await;
742 let res_bad_src = handle_bad_src.validate_integrity().await;
743 assert!(
744 res_bad_src.is_err(),
745 "Expected integrity check to fail with missing main.rs/lib.rs"
746 );
747
748 let (_tmp_dir, handle_no_readme) = create_crate_handle_in_temp(
750 "no_readme_crate",
751 "0.1.0",
752 true,
753 false,
754 false,
755 Some("main"),
756 )
757 .await;
758 let res_no_readme = handle_no_readme.validate_integrity().await;
759 assert!(res_no_readme.is_err(), "Expected missing README.md error");
760 match res_no_readme {
761 Err(CrateError::FileNotFound { missing_file }) => {
762 assert!(
763 missing_file.ends_with("README.md"),
764 "Expected error referencing missing README.md"
765 );
766 }
767 _ => panic!("Expected FileNotFound referencing README.md"),
768 }
769 }
770}