1use crate::adapters::PackageAdapter;
2use crate::errors::WorkspaceError;
3use crate::types::Workspace;
4use std::path::{Path, PathBuf};
5
6type Result<T> = std::result::Result<T, WorkspaceError>;
7
8pub fn discover_workspace(start_dir: &Path) -> Result<Workspace> {
10 let mut root = None;
11 let mut all_members = Vec::new();
12
13 for adapter in PackageAdapter::all() {
15 if adapter.can_discover(start_dir) {
16 let discovered_root = find_workspace_root_for_adapter(start_dir, *adapter)?;
18
19 let packages = adapter.discover(&discovered_root)?;
21
22 root.get_or_insert(discovered_root);
24 all_members.extend(packages);
25 }
26 }
27
28 let workspace_root = root.ok_or(WorkspaceError::NotFound)?;
29
30 Ok(Workspace {
31 root: workspace_root,
32 members: all_members,
33 })
34}
35
36fn find_workspace_root_for_adapter(start_dir: &Path, adapter: PackageAdapter) -> Result<PathBuf> {
38 let mut current = start_dir;
39 loop {
40 if adapter.can_discover(current) {
41 return Ok(current.to_path_buf());
42 }
43 current = current.parent().ok_or(WorkspaceError::NotFound)?;
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use crate::types::PackageKind;
51 use std::fs;
52
53 #[test]
54 fn discover_workspace_finds_cargo_packages() {
55 let temp = tempfile::tempdir().unwrap();
56 let root = temp.path();
57
58 fs::write(
60 root.join("Cargo.toml"),
61 "[workspace]\nmembers = [\"crates/*\"]\n",
62 )
63 .unwrap();
64
65 let crates_dir = root.join("crates");
67 fs::create_dir_all(crates_dir.join("pkg-a")).unwrap();
68 fs::create_dir_all(crates_dir.join("pkg-b")).unwrap();
69 fs::write(
70 crates_dir.join("pkg-a/Cargo.toml"),
71 "[package]\nname = \"pkg-a\"\nversion = \"0.1.0\"\n",
72 )
73 .unwrap();
74 fs::write(
75 crates_dir.join("pkg-b/Cargo.toml"),
76 "[package]\nname = \"pkg-b\"\nversion = \"0.2.0\"\n",
77 )
78 .unwrap();
79
80 let ws = discover_workspace(root).unwrap();
81 assert_eq!(ws.members.len(), 2);
82
83 let mut names: Vec<_> = ws.members.iter().map(|p| p.name.as_str()).collect();
84 names.sort();
85 assert_eq!(names, vec!["pkg-a", "pkg-b"]);
86 }
87
88 #[test]
89 fn discover_workspace_detects_internal_deps() {
90 let temp = tempfile::tempdir().unwrap();
91 let root = temp.path();
92
93 fs::write(
95 root.join("Cargo.toml"),
96 "[workspace]\nmembers = [\"crates/*\"]\n",
97 )
98 .unwrap();
99
100 let crates_dir = root.join("crates");
102 fs::create_dir_all(crates_dir.join("x")).unwrap();
103 fs::create_dir_all(crates_dir.join("y")).unwrap();
104 fs::create_dir_all(crates_dir.join("z")).unwrap();
105 fs::write(
106 crates_dir.join("x/Cargo.toml"),
107 format!(
108 "{}{}{}",
109 "[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
110 "[dependencies]\n",
111 "y={ path=\"../y\" }\n z={ workspace=true }\n"
112 ),
113 )
114 .unwrap();
115 fs::write(
116 crates_dir.join("y/Cargo.toml"),
117 "[package]\nname=\"y\"\nversion=\"0.1.0\"\n",
118 )
119 .unwrap();
120 fs::write(
121 crates_dir.join("z/Cargo.toml"),
122 "[package]\nname=\"z\"\nversion=\"0.1.0\"\n",
123 )
124 .unwrap();
125
126 let ws = discover_workspace(root).unwrap();
127 let x = ws.members.iter().find(|c| c.name == "x").unwrap();
128 assert!(x.internal_deps.contains("cargo/y"));
129 assert!(x.internal_deps.contains("cargo/z"));
130 }
131
132 #[test]
133 fn discover_workspace_returns_cargo_packages() {
134 let temp = tempfile::tempdir().unwrap();
138 let root = temp.path();
139
140 fs::write(
142 root.join("Cargo.toml"),
143 "[workspace]\nmembers = [\"crates/*\"]\n",
144 )
145 .unwrap();
146
147 let crates_dir = root.join("crates");
148 fs::create_dir_all(crates_dir.join("cargo-pkg")).unwrap();
149 fs::write(
150 crates_dir.join("cargo-pkg/Cargo.toml"),
151 "[package]\nname = \"cargo-pkg\"\nversion = \"1.0.0\"\n",
152 )
153 .unwrap();
154
155 let ws = discover_workspace(root).unwrap();
156
157 assert_eq!(ws.members.len(), 1);
159 assert_eq!(ws.members[0].name, "cargo-pkg");
160 assert_eq!(ws.members[0].kind, PackageKind::Cargo);
161 }
162
163 #[test]
164 fn discover_workspace_handles_empty_workspace() {
165 let temp = tempfile::tempdir().unwrap();
167 let root = temp.path();
168
169 fs::write(root.join("Cargo.toml"), "[workspace]\nmembers = []\n").unwrap();
170
171 let ws = discover_workspace(root).unwrap();
172 assert_eq!(ws.members.len(), 0);
173 assert_eq!(ws.root, root);
174 }
175
176 #[test]
177 fn discover_workspace_fails_when_no_workspace_found() {
178 let temp = tempfile::tempdir().unwrap();
180 let root = temp.path();
181
182 let result = discover_workspace(root);
184 assert!(result.is_err());
185
186 match result {
188 Err(WorkspaceError::NotFound) => {}
189 _ => panic!("Expected WorkspaceError::NotFound"),
190 }
191 }
192
193 #[test]
194 fn discover_workspace_discovers_npm_packages() {
195 let temp = tempfile::tempdir().unwrap();
196 let root = temp.path();
197
198 fs::write(
199 root.join("package.json"),
200 r#"{
201 "name": "root-package",
202 "version": "1.0.0",
203 "workspaces": ["packages/*"]
204}
205"#,
206 )
207 .unwrap();
208
209 let packages_dir = root.join("packages");
210 fs::create_dir_all(packages_dir.join("pkg-a")).unwrap();
211 fs::write(
212 packages_dir.join("pkg-a/package.json"),
213 r#"{
214 "name": "pkg-a",
215 "version": "0.1.0"
216}
217"#,
218 )
219 .unwrap();
220
221 let ws = discover_workspace(root).unwrap();
222 assert_eq!(ws.members.len(), 2);
223 assert!(
224 ws.members
225 .iter()
226 .any(|pkg| pkg.name == "root-package" && pkg.kind == PackageKind::Npm)
227 );
228 assert!(
229 ws.members
230 .iter()
231 .any(|pkg| pkg.name == "pkg-a" && pkg.kind == PackageKind::Npm)
232 );
233 }
234
235 #[test]
236 fn discover_workspace_discovers_hex_packages() {
237 let temp = tempfile::tempdir().unwrap();
238 let root = temp.path();
239
240 fs::write(
241 root.join("mix.exs"),
242 r#"
243defmodule Example.MixProject do
244 use Mix.Project
245
246 def project do
247 [
248 app: :example,
249 version: "0.1.0",
250 deps: deps()
251 ]
252 end
253
254 defp deps do
255 []
256 end
257end
258"#,
259 )
260 .unwrap();
261
262 let ws = discover_workspace(root).unwrap();
263 assert_eq!(ws.members.len(), 1);
264 assert_eq!(ws.members[0].name, "example");
265 assert_eq!(ws.members[0].kind, PackageKind::Hex);
266 }
267}