changepacks_rust/
finder.rs1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use changepacks_core::{Project, ProjectFinder};
4use std::{
5 collections::HashMap,
6 path::{Path, PathBuf},
7};
8use tokio::fs::read_to_string;
9
10use crate::{package::RustPackage, workspace::RustWorkspace};
11
12#[derive(Debug)]
13pub struct RustProjectFinder {
14 projects: HashMap<PathBuf, Project>,
15 project_files: Vec<&'static str>,
16}
17
18impl Default for RustProjectFinder {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl RustProjectFinder {
25 pub fn new() -> Self {
26 Self {
27 projects: HashMap::new(),
28 project_files: vec!["Cargo.toml"],
29 }
30 }
31}
32
33#[async_trait]
34impl ProjectFinder for RustProjectFinder {
35 fn projects(&self) -> Vec<&Project> {
36 self.projects.values().collect::<Vec<_>>()
37 }
38 fn projects_mut(&mut self) -> Vec<&mut Project> {
39 self.projects.values_mut().collect::<Vec<_>>()
40 }
41
42 fn project_files(&self) -> &[&str] {
43 &self.project_files
44 }
45
46 async fn visit(&mut self, path: &Path, relative_path: &Path) -> Result<()> {
47 if path.is_file()
48 && self.project_files().contains(
49 &path
50 .file_name()
51 .context(format!("File name not found - {}", path.display()))?
52 .to_str()
53 .context(format!("File name not found - {}", path.display()))?,
54 )
55 {
56 if self.projects.contains_key(path) {
57 return Ok(());
58 }
59 let cargo_toml = read_to_string(path).await?;
61 let cargo_toml: toml::Value = toml::from_str(&cargo_toml)?;
62 if cargo_toml.get("workspace").is_some() {
64 let version = cargo_toml
65 .get("package")
66 .and_then(|p| p.get("version"))
67 .and_then(|v| v.as_str())
68 .map(|v| v.to_string());
69 let name = cargo_toml
70 .get("package")
71 .and_then(|p| p.get("name"))
72 .and_then(|v| v.as_str())
73 .map(|v| v.to_string());
74 self.projects.insert(
75 path.to_path_buf(),
76 Project::Workspace(Box::new(RustWorkspace::new(
77 name,
78 version,
79 path.to_path_buf(),
80 relative_path.to_path_buf(),
81 ))),
82 );
83 } else {
84 let version = cargo_toml["package"]["version"]
85 .as_str()
86 .context(format!("Version not found - {}", path.display()))?
87 .to_string();
88 let name = cargo_toml["package"]["name"]
89 .as_str()
90 .context(format!("Name not found - {}", path.display()))?
91 .to_string();
92 self.projects.insert(
93 path.to_path_buf(),
94 Project::Package(Box::new(RustPackage::new(
95 name,
96 version,
97 path.to_path_buf(),
98 relative_path.to_path_buf(),
99 ))),
100 );
101 }
102 }
103 Ok(())
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use changepacks_core::Project;
111 use std::fs;
112 use tempfile::TempDir;
113
114 #[test]
115 fn test_rust_project_finder_new() {
116 let finder = RustProjectFinder::new();
117 assert_eq!(finder.project_files(), &["Cargo.toml"]);
118 assert_eq!(finder.projects().len(), 0);
119 }
120
121 #[test]
122 fn test_rust_project_finder_default() {
123 let finder = RustProjectFinder::default();
124 assert_eq!(finder.project_files(), &["Cargo.toml"]);
125 assert_eq!(finder.projects().len(), 0);
126 }
127
128 #[tokio::test]
129 async fn test_rust_project_finder_visit_package() {
130 let temp_dir = TempDir::new().unwrap();
131 let cargo_toml = temp_dir.path().join("Cargo.toml");
132 fs::write(
133 &cargo_toml,
134 r#"[package]
135name = "test-package"
136version = "1.0.0"
137"#,
138 )
139 .unwrap();
140
141 let mut finder = RustProjectFinder::new();
142 finder
143 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
144 .await
145 .unwrap();
146
147 let projects = finder.projects();
148 assert_eq!(projects.len(), 1);
149 match projects[0] {
150 Project::Package(pkg) => {
151 assert_eq!(pkg.name(), "test-package");
152 assert_eq!(pkg.version(), "1.0.0");
153 }
154 _ => panic!("Expected Package"),
155 }
156
157 temp_dir.close().unwrap();
158 }
159
160 #[tokio::test]
161 async fn test_rust_project_finder_visit_workspace() {
162 let temp_dir = TempDir::new().unwrap();
163 let cargo_toml = temp_dir.path().join("Cargo.toml");
164 fs::write(
165 &cargo_toml,
166 r#"[workspace]
167members = ["crates/*"]
168
169[package]
170name = "test-workspace"
171version = "1.0.0"
172"#,
173 )
174 .unwrap();
175
176 let mut finder = RustProjectFinder::new();
177 finder
178 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
179 .await
180 .unwrap();
181
182 let projects = finder.projects();
183 assert_eq!(projects.len(), 1);
184 match projects[0] {
185 Project::Workspace(ws) => {
186 assert_eq!(ws.name(), Some("test-workspace"));
187 assert_eq!(ws.version(), Some("1.0.0"));
188 }
189 _ => panic!("Expected Workspace"),
190 }
191
192 temp_dir.close().unwrap();
193 }
194
195 #[tokio::test]
196 async fn test_rust_project_finder_visit_workspace_without_package() {
197 let temp_dir = TempDir::new().unwrap();
198 let cargo_toml = temp_dir.path().join("Cargo.toml");
199 fs::write(
200 &cargo_toml,
201 r#"[workspace]
202members = ["crates/*"]
203"#,
204 )
205 .unwrap();
206
207 let mut finder = RustProjectFinder::new();
208 finder
209 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
210 .await
211 .unwrap();
212
213 let projects = finder.projects();
214 assert_eq!(projects.len(), 1);
215 match projects[0] {
216 Project::Workspace(ws) => {
217 assert_eq!(ws.name(), None);
218 assert_eq!(ws.version(), None);
219 }
220 _ => panic!("Expected Workspace"),
221 }
222
223 temp_dir.close().unwrap();
224 }
225
226 #[tokio::test]
227 async fn test_rust_project_finder_visit_non_cargo_file() {
228 let temp_dir = TempDir::new().unwrap();
229 let other_file = temp_dir.path().join("other.txt");
230 fs::write(&other_file, "some content").unwrap();
231
232 let mut finder = RustProjectFinder::new();
233 finder
234 .visit(&other_file, &PathBuf::from("other.txt"))
235 .await
236 .unwrap();
237
238 assert_eq!(finder.projects().len(), 0);
239
240 temp_dir.close().unwrap();
241 }
242
243 #[tokio::test]
244 async fn test_rust_project_finder_visit_directory() {
245 let temp_dir = TempDir::new().unwrap();
246 let cargo_toml = temp_dir.path().join("Cargo.toml");
247 fs::write(
248 &cargo_toml,
249 r#"[package]
250name = "test-package"
251version = "1.0.0"
252"#,
253 )
254 .unwrap();
255
256 let mut finder = RustProjectFinder::new();
257 finder
259 .visit(temp_dir.path(), &PathBuf::from("."))
260 .await
261 .unwrap();
262
263 assert_eq!(finder.projects().len(), 0);
264
265 temp_dir.close().unwrap();
266 }
267
268 #[tokio::test]
269 async fn test_rust_project_finder_visit_duplicate() {
270 let temp_dir = TempDir::new().unwrap();
271 let cargo_toml = temp_dir.path().join("Cargo.toml");
272 fs::write(
273 &cargo_toml,
274 r#"[package]
275name = "test-package"
276version = "1.0.0"
277"#,
278 )
279 .unwrap();
280
281 let mut finder = RustProjectFinder::new();
282 finder
283 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
284 .await
285 .unwrap();
286
287 assert_eq!(finder.projects().len(), 1);
288
289 finder
291 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
292 .await
293 .unwrap();
294
295 assert_eq!(finder.projects().len(), 1);
296
297 temp_dir.close().unwrap();
298 }
299
300 #[tokio::test]
301 async fn test_rust_project_finder_visit_multiple_packages() {
302 let temp_dir = TempDir::new().unwrap();
303 let cargo_toml1 = temp_dir.path().join("package1").join("Cargo.toml");
304 fs::create_dir_all(cargo_toml1.parent().unwrap()).unwrap();
305 fs::write(
306 &cargo_toml1,
307 r#"[package]
308name = "package1"
309version = "1.0.0"
310"#,
311 )
312 .unwrap();
313
314 let cargo_toml2 = temp_dir.path().join("package2").join("Cargo.toml");
315 fs::create_dir_all(cargo_toml2.parent().unwrap()).unwrap();
316 fs::write(
317 &cargo_toml2,
318 r#"[package]
319name = "package2"
320version = "2.0.0"
321"#,
322 )
323 .unwrap();
324
325 let mut finder = RustProjectFinder::new();
326 finder
327 .visit(&cargo_toml1, &PathBuf::from("package1/Cargo.toml"))
328 .await
329 .unwrap();
330 finder
331 .visit(&cargo_toml2, &PathBuf::from("package2/Cargo.toml"))
332 .await
333 .unwrap();
334
335 let projects = finder.projects();
336 assert_eq!(projects.len(), 2);
337
338 temp_dir.close().unwrap();
339 }
340
341 #[tokio::test]
342 async fn test_rust_project_finder_projects_mut() {
343 let temp_dir = TempDir::new().unwrap();
344 let cargo_toml = temp_dir.path().join("Cargo.toml");
345 fs::write(
346 &cargo_toml,
347 r#"[package]
348name = "test-package"
349version = "1.0.0"
350"#,
351 )
352 .unwrap();
353
354 let mut finder = RustProjectFinder::new();
355 finder
356 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
357 .await
358 .unwrap();
359
360 let mut_projects = finder.projects_mut();
361 assert_eq!(mut_projects.len(), 1);
362
363 temp_dir.close().unwrap();
364 }
365}