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 .map(|v| v.to_string());
87 let name = cargo_toml["package"]["name"]
88 .as_str()
89 .map(|v| v.to_string());
90 self.projects.insert(
91 path.to_path_buf(),
92 Project::Package(Box::new(RustPackage::new(
93 name,
94 version,
95 path.to_path_buf(),
96 relative_path.to_path_buf(),
97 ))),
98 );
99 }
100 }
101 Ok(())
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use changepacks_core::Project;
109 use std::fs;
110 use tempfile::TempDir;
111
112 #[test]
113 fn test_rust_project_finder_new() {
114 let finder = RustProjectFinder::new();
115 assert_eq!(finder.project_files(), &["Cargo.toml"]);
116 assert_eq!(finder.projects().len(), 0);
117 }
118
119 #[test]
120 fn test_rust_project_finder_default() {
121 let finder = RustProjectFinder::default();
122 assert_eq!(finder.project_files(), &["Cargo.toml"]);
123 assert_eq!(finder.projects().len(), 0);
124 }
125
126 #[tokio::test]
127 async fn test_rust_project_finder_visit_package() {
128 let temp_dir = TempDir::new().unwrap();
129 let cargo_toml = temp_dir.path().join("Cargo.toml");
130 fs::write(
131 &cargo_toml,
132 r#"[package]
133name = "test-package"
134version = "1.0.0"
135"#,
136 )
137 .unwrap();
138
139 let mut finder = RustProjectFinder::new();
140 finder
141 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
142 .await
143 .unwrap();
144
145 let projects = finder.projects();
146 assert_eq!(projects.len(), 1);
147 match projects[0] {
148 Project::Package(pkg) => {
149 assert_eq!(pkg.name(), Some("test-package"));
150 assert_eq!(pkg.version(), Some("1.0.0"));
151 }
152 _ => panic!("Expected Package"),
153 }
154
155 temp_dir.close().unwrap();
156 }
157
158 #[tokio::test]
159 async fn test_rust_project_finder_visit_workspace() {
160 let temp_dir = TempDir::new().unwrap();
161 let cargo_toml = temp_dir.path().join("Cargo.toml");
162 fs::write(
163 &cargo_toml,
164 r#"[workspace]
165members = ["crates/*"]
166
167[package]
168name = "test-workspace"
169version = "1.0.0"
170"#,
171 )
172 .unwrap();
173
174 let mut finder = RustProjectFinder::new();
175 finder
176 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
177 .await
178 .unwrap();
179
180 let projects = finder.projects();
181 assert_eq!(projects.len(), 1);
182 match projects[0] {
183 Project::Workspace(ws) => {
184 assert_eq!(ws.name(), Some("test-workspace"));
185 assert_eq!(ws.version(), Some("1.0.0"));
186 }
187 _ => panic!("Expected Workspace"),
188 }
189
190 temp_dir.close().unwrap();
191 }
192
193 #[tokio::test]
194 async fn test_rust_project_finder_visit_workspace_without_package() {
195 let temp_dir = TempDir::new().unwrap();
196 let cargo_toml = temp_dir.path().join("Cargo.toml");
197 fs::write(
198 &cargo_toml,
199 r#"[workspace]
200members = ["crates/*"]
201"#,
202 )
203 .unwrap();
204
205 let mut finder = RustProjectFinder::new();
206 finder
207 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
208 .await
209 .unwrap();
210
211 let projects = finder.projects();
212 assert_eq!(projects.len(), 1);
213 match projects[0] {
214 Project::Workspace(ws) => {
215 assert_eq!(ws.name(), None);
216 assert_eq!(ws.version(), None);
217 }
218 _ => panic!("Expected Workspace"),
219 }
220
221 temp_dir.close().unwrap();
222 }
223
224 #[tokio::test]
225 async fn test_rust_project_finder_visit_non_cargo_file() {
226 let temp_dir = TempDir::new().unwrap();
227 let other_file = temp_dir.path().join("other.txt");
228 fs::write(&other_file, "some content").unwrap();
229
230 let mut finder = RustProjectFinder::new();
231 finder
232 .visit(&other_file, &PathBuf::from("other.txt"))
233 .await
234 .unwrap();
235
236 assert_eq!(finder.projects().len(), 0);
237
238 temp_dir.close().unwrap();
239 }
240
241 #[tokio::test]
242 async fn test_rust_project_finder_visit_directory() {
243 let temp_dir = TempDir::new().unwrap();
244 let cargo_toml = temp_dir.path().join("Cargo.toml");
245 fs::write(
246 &cargo_toml,
247 r#"[package]
248name = "test-package"
249version = "1.0.0"
250"#,
251 )
252 .unwrap();
253
254 let mut finder = RustProjectFinder::new();
255 finder
257 .visit(temp_dir.path(), &PathBuf::from("."))
258 .await
259 .unwrap();
260
261 assert_eq!(finder.projects().len(), 0);
262
263 temp_dir.close().unwrap();
264 }
265
266 #[tokio::test]
267 async fn test_rust_project_finder_visit_duplicate() {
268 let temp_dir = TempDir::new().unwrap();
269 let cargo_toml = temp_dir.path().join("Cargo.toml");
270 fs::write(
271 &cargo_toml,
272 r#"[package]
273name = "test-package"
274version = "1.0.0"
275"#,
276 )
277 .unwrap();
278
279 let mut finder = RustProjectFinder::new();
280 finder
281 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
282 .await
283 .unwrap();
284
285 assert_eq!(finder.projects().len(), 1);
286
287 finder
289 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
290 .await
291 .unwrap();
292
293 assert_eq!(finder.projects().len(), 1);
294
295 temp_dir.close().unwrap();
296 }
297
298 #[tokio::test]
299 async fn test_rust_project_finder_visit_multiple_packages() {
300 let temp_dir = TempDir::new().unwrap();
301 let cargo_toml1 = temp_dir.path().join("package1").join("Cargo.toml");
302 fs::create_dir_all(cargo_toml1.parent().unwrap()).unwrap();
303 fs::write(
304 &cargo_toml1,
305 r#"[package]
306name = "package1"
307version = "1.0.0"
308"#,
309 )
310 .unwrap();
311
312 let cargo_toml2 = temp_dir.path().join("package2").join("Cargo.toml");
313 fs::create_dir_all(cargo_toml2.parent().unwrap()).unwrap();
314 fs::write(
315 &cargo_toml2,
316 r#"[package]
317name = "package2"
318version = "2.0.0"
319"#,
320 )
321 .unwrap();
322
323 let mut finder = RustProjectFinder::new();
324 finder
325 .visit(&cargo_toml1, &PathBuf::from("package1/Cargo.toml"))
326 .await
327 .unwrap();
328 finder
329 .visit(&cargo_toml2, &PathBuf::from("package2/Cargo.toml"))
330 .await
331 .unwrap();
332
333 let projects = finder.projects();
334 assert_eq!(projects.len(), 2);
335
336 temp_dir.close().unwrap();
337 }
338
339 #[tokio::test]
340 async fn test_rust_project_finder_projects_mut() {
341 let temp_dir = TempDir::new().unwrap();
342 let cargo_toml = temp_dir.path().join("Cargo.toml");
343 fs::write(
344 &cargo_toml,
345 r#"[package]
346name = "test-package"
347version = "1.0.0"
348"#,
349 )
350 .unwrap();
351
352 let mut finder = RustProjectFinder::new();
353 finder
354 .visit(&cargo_toml, &PathBuf::from("Cargo.toml"))
355 .await
356 .unwrap();
357
358 let mut_projects = finder.projects_mut();
359 assert_eq!(mut_projects.len(), 1);
360
361 temp_dir.close().unwrap();
362 }
363}