cargo_context_core/collect/
meta.rs1use std::path::{Path, PathBuf};
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::{Error, Result};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum DepKind {
16 Normal,
17 Dev,
18 Build,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct DependencyInfo {
23 pub name: String,
24 pub req: String,
25 pub kind: DepKind,
26 pub optional: bool,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct WorkspaceMember {
31 pub name: String,
32 pub version: String,
33 pub manifest_path: PathBuf,
34 pub dependencies: Vec<DependencyInfo>,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct WorkspaceMap {
39 pub workspace_root: PathBuf,
40 pub target_directory: PathBuf,
41 pub root_package: Option<String>,
42 pub members: Vec<WorkspaceMember>,
43}
44
45impl WorkspaceMap {
46 pub fn member_names(&self) -> Vec<&str> {
48 self.members.iter().map(|m| m.name.as_str()).collect()
49 }
50
51 pub fn external_dep_names(&self) -> Vec<&str> {
53 let mut names: Vec<&str> = self
54 .members
55 .iter()
56 .flat_map(|m| m.dependencies.iter())
57 .filter(|d| d.kind == DepKind::Normal)
58 .map(|d| d.name.as_str())
59 .collect();
60 names.sort_unstable();
61 names.dedup();
62 let member_names: std::collections::HashSet<&str> =
64 self.member_names().into_iter().collect();
65 names.retain(|n| !member_names.contains(n));
66 names
67 }
68}
69
70pub fn cargo_metadata(root: &Path) -> Result<WorkspaceMap> {
74 let meta = cargo_metadata::MetadataCommand::new()
75 .current_dir(root)
76 .no_deps()
77 .exec()
78 .map_err(|e| Error::Tool(format!("cargo metadata failed: {e}")))?;
79
80 let root_package = meta.root_package().map(|p| p.name.to_string());
81 let members: Vec<WorkspaceMember> = meta
82 .workspace_packages()
83 .into_iter()
84 .map(|p| WorkspaceMember {
85 name: p.name.to_string(),
86 version: p.version.to_string(),
87 manifest_path: p.manifest_path.clone().into_std_path_buf(),
88 dependencies: p
89 .dependencies
90 .iter()
91 .map(|d| DependencyInfo {
92 name: d.name.clone(),
93 req: d.req.to_string(),
94 kind: map_dep_kind(&d.kind),
95 optional: d.optional,
96 })
97 .collect(),
98 })
99 .collect();
100
101 Ok(WorkspaceMap {
102 workspace_root: meta.workspace_root.clone().into_std_path_buf(),
103 target_directory: meta.target_directory.clone().into_std_path_buf(),
104 root_package,
105 members,
106 })
107}
108
109fn map_dep_kind(k: &cargo_metadata::DependencyKind) -> DepKind {
110 use cargo_metadata::DependencyKind as K;
111 match k {
112 K::Normal => DepKind::Normal,
113 K::Development => DepKind::Dev,
114 K::Build => DepKind::Build,
115 _ => DepKind::Normal,
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
126 fn reads_own_workspace() {
127 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
128 let map = cargo_metadata(root).expect("read metadata");
129 assert!(
130 map.members.iter().any(|m| m.name == "cargo-context-core"),
131 "expected cargo-context-core in members, got: {:?}",
132 map.member_names()
133 );
134 }
135
136 #[test]
137 fn external_deps_exclude_workspace_members() {
138 let root = Path::new(env!("CARGO_MANIFEST_DIR"));
139 let map = cargo_metadata(root).expect("read metadata");
140 let ext = map.external_dep_names();
141 for member in map.member_names() {
142 assert!(
143 !ext.contains(&member),
144 "workspace member {member} leaked into external deps"
145 );
146 }
147 }
148}