Skip to main content

crate_seq_core/
workspace.rs

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