1use std::path::{Path, PathBuf};
7
8use cargo_metadata::{MetadataCommand, Package};
9
10use crate::error::RustlocError;
11use crate::Result;
12
13#[derive(Debug, Clone)]
15pub struct CrateInfo {
16 pub name: String,
18 pub root: PathBuf,
20 pub src_dirs: Vec<PathBuf>,
22 pub tests_dir: Option<PathBuf>,
24 pub examples_dir: Option<PathBuf>,
26 pub benches_dir: Option<PathBuf>,
28}
29
30impl CrateInfo {
31 fn from_package(package: &Package) -> Self {
33 let root = package
34 .manifest_path
35 .parent()
36 .map(|p| p.to_path_buf().into_std_path_buf())
37 .unwrap_or_default();
38
39 let src_dir = root.join("src");
40 let tests_dir = root.join("tests");
41 let examples_dir = root.join("examples");
42 let benches_dir = root.join("benches");
43
44 Self {
45 name: package.name.clone(),
46 root: root.clone(),
47 src_dirs: if src_dir.exists() {
48 vec![src_dir]
49 } else {
50 vec![]
51 },
52 tests_dir: if tests_dir.exists() {
53 Some(tests_dir)
54 } else {
55 None
56 },
57 examples_dir: if examples_dir.exists() {
58 Some(examples_dir)
59 } else {
60 None
61 },
62 benches_dir: if benches_dir.exists() {
63 Some(benches_dir)
64 } else {
65 None
66 },
67 }
68 }
69
70 pub fn all_dirs(&self) -> Vec<&Path> {
72 let mut dirs: Vec<&Path> = self.src_dirs.iter().map(|p| p.as_path()).collect();
73
74 if let Some(ref tests) = self.tests_dir {
75 dirs.push(tests.as_path());
76 }
77 if let Some(ref examples) = self.examples_dir {
78 dirs.push(examples.as_path());
79 }
80 if let Some(ref benches) = self.benches_dir {
81 dirs.push(benches.as_path());
82 }
83
84 dirs
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct WorkspaceInfo {
91 pub root: PathBuf,
93 pub crates: Vec<CrateInfo>,
95}
96
97impl WorkspaceInfo {
98 pub fn discover(path: impl AsRef<Path>) -> Result<Self> {
105 let path = path.as_ref();
106
107 let manifest_path = if path.is_file() && path.file_name() == Some("Cargo.toml".as_ref()) {
109 path.to_path_buf()
110 } else if path.is_dir() {
111 let cargo_toml = path.join("Cargo.toml");
112 if cargo_toml.exists() {
113 cargo_toml
114 } else {
115 return Err(RustlocError::PathNotFound(path.to_path_buf()));
116 }
117 } else {
118 return Err(RustlocError::PathNotFound(path.to_path_buf()));
119 };
120
121 let metadata = MetadataCommand::new()
122 .manifest_path(&manifest_path)
123 .exec()
124 .map_err(|e| RustlocError::CargoMetadata(e.to_string()))?;
125
126 let root = metadata.workspace_root.into_std_path_buf();
127
128 let workspace_members: std::collections::HashSet<_> =
130 metadata.workspace_members.iter().collect();
131
132 let crates: Vec<CrateInfo> = metadata
133 .packages
134 .iter()
135 .filter(|p| workspace_members.contains(&p.id))
136 .map(CrateInfo::from_package)
137 .collect();
138
139 Ok(Self { root, crates })
140 }
141
142 pub fn filter_by_names(&self, names: &[&str]) -> Self {
147 let crates = self
148 .crates
149 .iter()
150 .filter(|c| names.contains(&c.name.as_str()))
151 .cloned()
152 .collect();
153
154 Self {
155 root: self.root.clone(),
156 crates,
157 }
158 }
159
160 pub fn get_crate(&self, name: &str) -> Option<&CrateInfo> {
162 self.crates.iter().find(|c| c.name == name)
163 }
164
165 pub fn crate_names(&self) -> Vec<&str> {
167 self.crates.iter().map(|c| c.name.as_str()).collect()
168 }
169}
170
171pub fn is_cargo_project(path: impl AsRef<Path>) -> bool {
173 let path = path.as_ref();
174 if path.is_dir() {
175 path.join("Cargo.toml").exists()
176 } else {
177 path.file_name() == Some("Cargo.toml".as_ref()) && path.exists()
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_crate_info_all_dirs() {
187 let info = CrateInfo {
189 name: "test-crate".to_string(),
190 root: PathBuf::from("/project"),
191 src_dirs: vec![PathBuf::from("/project/src")],
192 tests_dir: Some(PathBuf::from("/project/tests")),
193 examples_dir: Some(PathBuf::from("/project/examples")),
194 benches_dir: None,
195 };
196
197 let dirs = info.all_dirs();
198 assert_eq!(dirs.len(), 3);
199 assert!(dirs.contains(&Path::new("/project/src")));
200 assert!(dirs.contains(&Path::new("/project/tests")));
201 assert!(dirs.contains(&Path::new("/project/examples")));
202 }
203
204 #[test]
205 fn test_workspace_filter_by_names() {
206 let workspace = WorkspaceInfo {
207 root: PathBuf::from("/workspace"),
208 crates: vec![
209 CrateInfo {
210 name: "crate-a".to_string(),
211 root: PathBuf::from("/workspace/crate-a"),
212 src_dirs: vec![],
213 tests_dir: None,
214 examples_dir: None,
215 benches_dir: None,
216 },
217 CrateInfo {
218 name: "crate-b".to_string(),
219 root: PathBuf::from("/workspace/crate-b"),
220 src_dirs: vec![],
221 tests_dir: None,
222 examples_dir: None,
223 benches_dir: None,
224 },
225 CrateInfo {
226 name: "crate-c".to_string(),
227 root: PathBuf::from("/workspace/crate-c"),
228 src_dirs: vec![],
229 tests_dir: None,
230 examples_dir: None,
231 benches_dir: None,
232 },
233 ],
234 };
235
236 let filtered = workspace.filter_by_names(&["crate-a", "crate-c"]);
237 assert_eq!(filtered.crates.len(), 2);
238 assert!(filtered.get_crate("crate-a").is_some());
239 assert!(filtered.get_crate("crate-b").is_none());
240 assert!(filtered.get_crate("crate-c").is_some());
241 }
242
243 #[test]
244 fn test_crate_names() {
245 let workspace = WorkspaceInfo {
246 root: PathBuf::from("/workspace"),
247 crates: vec![
248 CrateInfo {
249 name: "alpha".to_string(),
250 root: PathBuf::from("/workspace/alpha"),
251 src_dirs: vec![],
252 tests_dir: None,
253 examples_dir: None,
254 benches_dir: None,
255 },
256 CrateInfo {
257 name: "beta".to_string(),
258 root: PathBuf::from("/workspace/beta"),
259 src_dirs: vec![],
260 tests_dir: None,
261 examples_dir: None,
262 benches_dir: None,
263 },
264 ],
265 };
266
267 let names = workspace.crate_names();
268 assert_eq!(names, vec!["alpha", "beta"]);
269 }
270
271 #[test]
272 fn test_is_cargo_project() {
273 let temp = tempfile::tempdir().unwrap();
275 let temp_path = temp.path();
276
277 assert!(!is_cargo_project(temp_path));
279
280 std::fs::write(temp_path.join("Cargo.toml"), "[package]\nname = \"test\"").unwrap();
282
283 assert!(is_cargo_project(temp_path));
285 assert!(is_cargo_project(temp_path.join("Cargo.toml")));
286 }
287}