sampo_core/
workspace.rs

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
8/// Discover workspace packages using registered ecosystem adapters.
9pub fn discover_workspace(start_dir: &Path) -> Result<Workspace> {
10    let mut root = None;
11    let mut all_members = Vec::new();
12
13    // Try each registered adapter (static dispatch, zero-cost)
14    for adapter in PackageAdapter::all() {
15        if adapter.can_discover(start_dir) {
16            // Find the workspace root by walking up from start_dir
17            let discovered_root = find_workspace_root_for_adapter(start_dir, *adapter)?;
18
19            // Discover packages in this ecosystem
20            let packages = adapter.discover(&discovered_root)?;
21
22            // Use the first discovered root as the workspace root
23            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
36/// Find the workspace root for a given adapter by walking up the directory tree.
37fn 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        // Create workspace
59        fs::write(
60            root.join("Cargo.toml"),
61            "[workspace]\nmembers = [\"crates/*\"]\n",
62        )
63        .unwrap();
64
65        // Create crates
66        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        // workspace
94        fs::write(
95            root.join("Cargo.toml"),
96            "[workspace]\nmembers = [\"crates/*\"]\n",
97        )
98        .unwrap();
99
100        // crates: x depends on y via path, and on z via workspace
101        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        // This test verifies that when a workspace contains only Cargo packages,
135        // they are discovered correctly and marked with the Cargo kind.
136
137        let temp = tempfile::tempdir().unwrap();
138        let root = temp.path();
139
140        // Create a Cargo workspace
141        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        // Should discover the Cargo package
158        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        // Test that an empty workspace (workspace defined but no packages) is valid
166        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        // Test that we get an error when there's no workspace at all
179        let temp = tempfile::tempdir().unwrap();
180        let root = temp.path();
181
182        // No Cargo.toml, no workspace
183        let result = discover_workspace(root);
184        assert!(result.is_err());
185
186        // Verify it's the right error
187        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}