Skip to main content

crate_seq_core/
workspace.rs

1//! Workspace member discovery from a root `Cargo.toml`.
2
3#[cfg(test)]
4#[path = "workspace_tests.rs"]
5mod tests;
6
7use std::path::{Path, PathBuf};
8
9use crate::Error;
10
11/// A single member crate discovered from a workspace `Cargo.toml`.
12#[derive(Debug, Clone)]
13pub struct WorkspaceMember {
14    /// Crate name as declared in the member's `[package]` table.
15    pub name: String,
16    /// Absolute path to the member's `Cargo.toml`.
17    pub manifest_path: PathBuf,
18    /// Absolute path to the member's crate directory.
19    pub crate_dir: PathBuf,
20}
21
22/// Reads the file at `path` into a string, mapping the error to [`Error::Io`].
23fn read_file(path: &Path) -> Result<String, Error> {
24    std::fs::read_to_string(path).map_err(|source| Error::Io {
25        path: path.to_owned(),
26        source,
27    })
28}
29
30/// Parses the string as a TOML document, mapping errors to [`Error::Toml`].
31fn parse_toml(content: &str, path: &Path) -> Result<toml_edit::DocumentMut, Error> {
32    content
33        .parse::<toml_edit::DocumentMut>()
34        .map_err(|source| Error::Toml {
35            path: path.to_owned(),
36            source,
37        })
38}
39
40/// Extracts `[package].name` from a parsed TOML document.
41fn package_name(doc: &toml_edit::DocumentMut, manifest_path: &Path) -> Result<String, Error> {
42    doc.get("package")
43        .and_then(|p| p.get("name"))
44        .and_then(|n| n.as_str())
45        .map(str::to_owned)
46        .ok_or_else(|| Error::MissingPackageName(manifest_path.to_owned()))
47}
48
49/// Builds a [`WorkspaceMember`] by reading the `Cargo.toml` inside `member_dir`.
50fn member_from_dir(member_dir: PathBuf) -> Result<WorkspaceMember, Error> {
51    let manifest_path = member_dir.join("Cargo.toml");
52    if !manifest_path.exists() {
53        return Err(Error::Io {
54            source: std::io::Error::new(std::io::ErrorKind::NotFound, "Cargo.toml not found"),
55            path: manifest_path,
56        });
57    }
58    let content = read_file(&manifest_path)?;
59    let doc = parse_toml(&content, &manifest_path)?;
60    let name = package_name(&doc, &manifest_path)?;
61    Ok(WorkspaceMember {
62        name,
63        manifest_path,
64        crate_dir: member_dir,
65    })
66}
67
68/// Expands a member path entry that may contain a trailing `/*` glob.
69///
70/// Literal paths are returned as-is (wrapped in a one-element vec). Paths
71/// ending in `/*` are expanded with `read_dir` on the parent segment. Entries
72/// without a `Cargo.toml` are silently skipped.
73fn expand_member_path(workspace_root: &Path, raw: &str) -> Vec<PathBuf> {
74    if let Some(prefix) = raw.strip_suffix("/*") {
75        let parent = workspace_root.join(prefix);
76        match std::fs::read_dir(&parent) {
77            Ok(entries) => entries
78                .filter_map(std::result::Result::ok)
79                .map(|e| e.path())
80                .filter(|p| p.join("Cargo.toml").exists())
81                .collect(),
82            Err(_) => vec![],
83        }
84    } else {
85        let candidate = workspace_root.join(raw);
86        if candidate.join("Cargo.toml").exists() {
87            vec![candidate]
88        } else {
89            vec![]
90        }
91    }
92}
93
94/// Discovers workspace members from a root `Cargo.toml`.
95///
96/// Returns an error if the file is not a valid TOML workspace manifest.
97/// For a single-crate (non-workspace) manifest, returns a single member
98/// representing the root crate itself.
99///
100/// # Errors
101///
102/// Returns [`Error::Io`] if any file cannot be read, [`Error::Toml`] for
103/// parse failures, or [`Error::MissingPackageName`] if a member has no
104/// `[package].name`.
105pub fn discover_members(workspace_root: &Path) -> Result<Vec<WorkspaceMember>, Error> {
106    let root_manifest = workspace_root.join("Cargo.toml");
107    let content = read_file(&root_manifest)?;
108    let doc = parse_toml(&content, &root_manifest)?;
109
110    if let Some(members_array) = doc
111        .get("workspace")
112        .and_then(|w| w.get("members"))
113        .and_then(|m| m.as_array())
114    {
115        let raw_paths: Vec<String> = members_array
116            .iter()
117            .filter_map(|v| v.as_str().map(str::to_owned))
118            .collect();
119
120        let mut members = Vec::new();
121        for raw in &raw_paths {
122            for dir in expand_member_path(workspace_root, raw) {
123                members.push(member_from_dir(dir)?);
124            }
125        }
126        Ok(members)
127    } else {
128        let name = package_name(&doc, &root_manifest)?;
129        Ok(vec![WorkspaceMember {
130            name,
131            manifest_path: root_manifest,
132            crate_dir: workspace_root.to_owned(),
133        }])
134    }
135}