rust_docs_mcp/cache/
workspace.rs

1//! Workspace handling utilities for Rust crates
2//!
3//! This module provides functionality for detecting and managing Rust workspace crates,
4//! including member detection and metadata extraction.
5
6use anyhow::{Context, Result, anyhow};
7use std::fs;
8use std::path::Path;
9use toml::Value;
10
11/// Workspace-related utilities
12pub struct WorkspaceHandler;
13
14impl WorkspaceHandler {
15    /// Check if a Cargo.toml represents a workspace (virtual or mixed)
16    pub fn is_workspace(cargo_toml_path: &Path) -> Result<bool> {
17        let content = fs::read_to_string(cargo_toml_path).with_context(|| {
18            format!("Failed to read Cargo.toml at {}", cargo_toml_path.display())
19        })?;
20
21        let parsed: Value = toml::from_str(&content).with_context(|| {
22            format!(
23                "Failed to parse Cargo.toml at {}",
24                cargo_toml_path.display()
25            )
26        })?;
27
28        // Check if this is a workspace by looking for [workspace] section with members
29        if let Some(workspace) = parsed.get("workspace")
30            && let Some(members) = workspace.get("members")
31            && let Some(members_arr) = members.as_array()
32        {
33            // It's a workspace if it has workspace.members with at least one member
34            return Ok(!members_arr.is_empty());
35        }
36
37        Ok(false)
38    }
39
40    /// Get workspace members from a workspace Cargo.toml
41    pub fn get_workspace_members(cargo_toml_path: &Path) -> Result<Vec<String>> {
42        let content = fs::read_to_string(cargo_toml_path).with_context(|| {
43            format!("Failed to read Cargo.toml at {}", cargo_toml_path.display())
44        })?;
45
46        let parsed: Value = toml::from_str(&content).with_context(|| {
47            format!(
48                "Failed to parse Cargo.toml at {}",
49                cargo_toml_path.display()
50            )
51        })?;
52
53        let workspace = parsed
54            .get("workspace")
55            .ok_or_else(|| anyhow!("No [workspace] section found in Cargo.toml"))?;
56
57        let members = workspace
58            .get("members")
59            .and_then(|m| m.as_array())
60            .ok_or_else(|| anyhow!("No members array found in [workspace] section"))?;
61
62        let mut member_list = Vec::new();
63        for member in members {
64            if let Some(member_str) = member.as_str() {
65                // Expand glob patterns
66                if member_str.contains('*') {
67                    // For now, we'll skip glob patterns and handle them later if needed
68                    // In the real implementation, we'd expand these patterns
69                    if member_str == "examples/*" {
70                        // Skip examples for now as requested
71                        continue;
72                    }
73                } else {
74                    member_list.push(member_str.to_string());
75                }
76            }
77        }
78
79        Ok(member_list)
80    }
81
82    /// Get the package name from a Cargo.toml file
83    pub fn get_package_name(cargo_toml_path: &Path) -> Result<String> {
84        let content = fs::read_to_string(cargo_toml_path).with_context(|| {
85            format!("Failed to read Cargo.toml at {}", cargo_toml_path.display())
86        })?;
87
88        let parsed: Value = toml::from_str(&content).with_context(|| {
89            format!(
90                "Failed to parse Cargo.toml at {}",
91                cargo_toml_path.display()
92            )
93        })?;
94
95        let package = parsed
96            .get("package")
97            .ok_or_else(|| anyhow!("No [package] section found in Cargo.toml"))?;
98
99        let name = package
100            .get("name")
101            .and_then(|n| n.as_str())
102            .ok_or_else(|| anyhow!("No 'name' field found in [package] section"))?;
103
104        Ok(name.to_string())
105    }
106
107    /// Get the package version from a Cargo.toml file
108    pub fn get_package_version(cargo_toml_path: &Path) -> Result<String> {
109        let content = fs::read_to_string(cargo_toml_path).with_context(|| {
110            format!("Failed to read Cargo.toml at {}", cargo_toml_path.display())
111        })?;
112
113        let parsed: Value = toml::from_str(&content).with_context(|| {
114            format!(
115                "Failed to parse Cargo.toml at {}",
116                cargo_toml_path.display()
117            )
118        })?;
119
120        let package = parsed
121            .get("package")
122            .ok_or_else(|| anyhow!("No [package] section found in Cargo.toml"))?;
123
124        let version = package
125            .get("version")
126            .and_then(|v| v.as_str())
127            .ok_or_else(|| anyhow!("No 'version' field found in [package] section"))?;
128
129        Ok(version.to_string())
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use tempfile::TempDir;
137
138    #[test]
139    fn test_get_package_version() -> Result<()> {
140        let temp_dir = TempDir::new()?;
141
142        // Test regular crate with version
143        let cargo_toml = temp_dir.path().join("Cargo.toml");
144        fs::write(
145            &cargo_toml,
146            r#"
147[package]
148name = "test-crate"
149version = "1.2.3"
150"#,
151        )?;
152
153        let version = WorkspaceHandler::get_package_version(&cargo_toml)?;
154        assert_eq!(version, "1.2.3");
155
156        // Test crate without version field
157        let no_version_toml = temp_dir.path().join("no_version.toml");
158        fs::write(
159            &no_version_toml,
160            r#"
161[package]
162name = "test-crate"
163"#,
164        )?;
165
166        let result = WorkspaceHandler::get_package_version(&no_version_toml);
167        assert!(result.is_err());
168        assert!(
169            result
170                .unwrap_err()
171                .to_string()
172                .contains("No 'version' field")
173        );
174
175        Ok(())
176    }
177
178    #[test]
179    fn test_workspace_detection() -> Result<()> {
180        let temp_dir = TempDir::new()?;
181
182        // Test virtual manifest (workspace without package)
183        let workspace_toml = temp_dir.path().join("workspace.toml");
184        fs::write(
185            &workspace_toml,
186            r#"
187[workspace]
188members = ["crate-a", "crate-b"]
189"#,
190        )?;
191        assert!(WorkspaceHandler::is_workspace(&workspace_toml)?);
192
193        // Test regular crate (has package)
194        let crate_toml = temp_dir.path().join("crate.toml");
195        fs::write(
196            &crate_toml,
197            r#"
198[package]
199name = "my-crate"
200version = "0.1.0"
201"#,
202        )?;
203        assert!(!WorkspaceHandler::is_workspace(&crate_toml)?);
204
205        // Test workspace with package (mixed workspace)
206        let mixed_toml = temp_dir.path().join("mixed.toml");
207        fs::write(
208            &mixed_toml,
209            r#"
210[package]
211name = "my-crate"
212version = "0.1.0"
213
214[workspace]
215members = ["sub-crate"]
216"#,
217        )?;
218        assert!(WorkspaceHandler::is_workspace(&mixed_toml)?); // Mixed workspaces should be detected as workspaces
219
220        Ok(())
221    }
222}