clap_cargo/
workspace.rs

1//! Cargo flags for selecting crates in a workspace.
2
3/// Cargo flags for selecting crates in a workspace.
4#[derive(Default, Clone, Debug, PartialEq, Eq)]
5#[cfg_attr(feature = "clap", derive(clap::Args))]
6#[cfg_attr(feature = "clap", command(about = None, long_about = None))]
7#[non_exhaustive]
8pub struct Workspace {
9    #[cfg_attr(feature = "clap", arg(short, long, value_name = "SPEC"))]
10    /// Package to process (see `cargo help pkgid`)
11    pub package: Vec<String>,
12    #[cfg_attr(feature = "clap", arg(long))]
13    /// Process all packages in the workspace
14    pub workspace: bool,
15    #[cfg_attr(
16        feature = "clap",
17        arg(long, hide_short_help(true), hide_long_help(true))
18    )]
19    /// Process all packages in the workspace
20    pub all: bool,
21    #[cfg_attr(feature = "clap", arg(long, value_name = "SPEC"))]
22    /// Exclude packages from being processed
23    pub exclude: Vec<String>,
24}
25
26#[cfg(feature = "cargo_metadata")]
27impl Workspace {
28    /// Partition workspace members into those selected and those excluded.
29    ///
30    /// Notes:
31    /// - Requires the features `cargo_metadata`.
32    /// - Requires not calling `MetadataCommand::no_deps`
33    pub fn partition_packages<'m>(
34        &self,
35        meta: &'m cargo_metadata::Metadata,
36    ) -> (
37        Vec<&'m cargo_metadata::Package>,
38        Vec<&'m cargo_metadata::Package>,
39    ) {
40        let selection =
41            Packages::from_flags(self.workspace || self.all, &self.exclude, &self.package);
42        let workspace_members: std::collections::HashSet<_> =
43            meta.workspace_members.iter().collect();
44        let workspace_default_members: std::collections::HashSet<_> =
45            meta.workspace_default_members.iter().collect();
46        let base_ids: std::collections::HashSet<_> = match selection {
47            Packages::Default => workspace_default_members,
48            Packages::All => workspace_members,
49            Packages::OptOut(_) => workspace_members, // Deviating from cargo by only checking workspace members
50            Packages::Packages(patterns) => {
51                meta.packages
52                    .iter()
53                    // Deviating from cargo by not supporting patterns
54                    // Deviating from cargo by only checking workspace members
55                    .filter(|p| workspace_members.contains(&p.id) && patterns.contains(&p.name))
56                    .map(|p| &p.id)
57                    .collect()
58            }
59        };
60
61        meta.packages
62            .iter()
63            // Deviating from cargo by not supporting patterns
64            .partition(|p| base_ids.contains(&p.id) && !self.exclude.contains(&p.name))
65    }
66}
67
68// See cargo's src/cargo/ops/cargo_compile.rs
69#[derive(Clone, PartialEq, Eq, Debug)]
70#[cfg(feature = "cargo_metadata")]
71#[allow(clippy::enum_variant_names)]
72enum Packages<'p> {
73    Default,
74    All,
75    OptOut(&'p [String]),
76    Packages(&'p [String]),
77}
78
79#[cfg(feature = "cargo_metadata")]
80impl<'p> Packages<'p> {
81    fn from_flags(all: bool, exclude: &'p [String], package: &'p [String]) -> Self {
82        match (all, exclude.len(), package.len()) {
83            (false, 0, 0) => Packages::Default,
84            (false, 0, _) => Packages::Packages(package),
85            (false, _, 0) => Packages::OptOut(exclude), // Deviating from cargo because we don't do error handling
86            (false, _, _) => Packages::Packages(package), // Deviating from cargo because we don't do error handling
87            (true, 0, _) => Packages::All,
88            (true, _, _) => Packages::OptOut(exclude),
89        }
90    }
91}
92
93#[cfg(test)]
94mod test {
95    use super::*;
96
97    #[test]
98    #[cfg(feature = "clap")]
99    fn verify_app() {
100        #[derive(Debug, clap::Parser)]
101        struct Cli {
102            #[command(flatten)]
103            workspace: Workspace,
104        }
105
106        use clap::CommandFactory;
107        Cli::command().debug_assert();
108    }
109
110    #[test]
111    #[cfg(feature = "clap")]
112    fn parse_multiple_occurrences() {
113        use clap::Parser;
114
115        #[derive(PartialEq, Eq, Debug, Parser)]
116        struct Args {
117            positional: Option<String>,
118            #[command(flatten)]
119            workspace: Workspace,
120        }
121
122        assert_eq!(
123            Args {
124                positional: None,
125                workspace: Workspace {
126                    package: vec![],
127                    workspace: false,
128                    all: false,
129                    exclude: vec![],
130                }
131            },
132            Args::parse_from(["test"])
133        );
134        assert_eq!(
135            Args {
136                positional: Some("baz".to_owned()),
137                workspace: Workspace {
138                    package: vec!["foo".to_owned(), "bar".to_owned()],
139                    workspace: false,
140                    all: false,
141                    exclude: vec![],
142                }
143            },
144            Args::parse_from(["test", "--package", "foo", "--package", "bar", "baz"])
145        );
146        assert_eq!(
147            Args {
148                positional: Some("baz".to_owned()),
149                workspace: Workspace {
150                    package: vec![],
151                    workspace: false,
152                    all: false,
153                    exclude: vec!["foo".to_owned(), "bar".to_owned()],
154                }
155            },
156            Args::parse_from(["test", "--exclude", "foo", "--exclude", "bar", "baz"])
157        );
158    }
159
160    #[cfg(feature = "cargo_metadata")]
161    #[cfg(test)]
162    mod partition_default {
163        use super::*;
164
165        #[test]
166        fn single_crate() {
167            let mut metadata = cargo_metadata::MetadataCommand::new();
168            metadata.manifest_path("tests/fixtures/simple/Cargo.toml");
169            let metadata = metadata.exec().unwrap();
170
171            let workspace = Workspace {
172                ..Default::default()
173            };
174            let (included, excluded) = workspace.partition_packages(&metadata);
175            assert_eq!(included.len(), 1);
176            assert_eq!(excluded.len(), 0);
177        }
178
179        #[test]
180        fn mixed_ws_root() {
181            let mut metadata = cargo_metadata::MetadataCommand::new();
182            metadata.manifest_path("tests/fixtures/mixed_ws/Cargo.toml");
183            let metadata = metadata.exec().unwrap();
184
185            let workspace = Workspace {
186                ..Default::default()
187            };
188            let (included, excluded) = workspace.partition_packages(&metadata);
189            assert_eq!(included.len(), 1);
190            assert_eq!(excluded.len(), 2);
191        }
192
193        #[test]
194        fn mixed_ws_leaf() {
195            let mut metadata = cargo_metadata::MetadataCommand::new();
196            metadata.manifest_path("tests/fixtures/mixed_ws/c/Cargo.toml");
197            let metadata = metadata.exec().unwrap();
198
199            let workspace = Workspace {
200                ..Default::default()
201            };
202            let (included, excluded) = workspace.partition_packages(&metadata);
203            assert_eq!(included.len(), 1);
204            assert_eq!(excluded.len(), 2);
205        }
206
207        #[test]
208        fn pure_ws_root() {
209            let mut metadata = cargo_metadata::MetadataCommand::new();
210            metadata.manifest_path("tests/fixtures/pure_ws/Cargo.toml");
211            let metadata = metadata.exec().unwrap();
212
213            let workspace = Workspace {
214                ..Default::default()
215            };
216            let (included, excluded) = workspace.partition_packages(&metadata);
217            assert_eq!(included.len(), 3);
218            assert_eq!(excluded.len(), 0);
219        }
220
221        #[test]
222        fn pure_ws_leaf() {
223            let mut metadata = cargo_metadata::MetadataCommand::new();
224            metadata.manifest_path("tests/fixtures/pure_ws/c/Cargo.toml");
225            let metadata = metadata.exec().unwrap();
226
227            let workspace = Workspace {
228                ..Default::default()
229            };
230            let (included, excluded) = workspace.partition_packages(&metadata);
231            assert_eq!(included.len(), 1);
232            assert_eq!(excluded.len(), 2);
233        }
234    }
235
236    #[cfg(feature = "cargo_metadata")]
237    #[cfg(test)]
238    mod partition_all {
239        use super::*;
240
241        #[test]
242        fn single_crate() {
243            let mut metadata = cargo_metadata::MetadataCommand::new();
244            metadata.manifest_path("tests/fixtures/simple/Cargo.toml");
245            let metadata = metadata.exec().unwrap();
246
247            let workspace = Workspace {
248                all: true,
249                ..Default::default()
250            };
251            let (included, excluded) = workspace.partition_packages(&metadata);
252            assert_eq!(included.len(), 1);
253            assert_eq!(excluded.len(), 0);
254        }
255
256        #[test]
257        fn mixed_ws_root() {
258            let mut metadata = cargo_metadata::MetadataCommand::new();
259            metadata.manifest_path("tests/fixtures/mixed_ws/Cargo.toml");
260            let metadata = metadata.exec().unwrap();
261
262            let workspace = Workspace {
263                all: true,
264                ..Default::default()
265            };
266            let (included, excluded) = workspace.partition_packages(&metadata);
267            assert_eq!(included.len(), 3);
268            assert_eq!(excluded.len(), 0);
269        }
270
271        #[test]
272        fn mixed_ws_leaf() {
273            let mut metadata = cargo_metadata::MetadataCommand::new();
274            metadata.manifest_path("tests/fixtures/mixed_ws/c/Cargo.toml");
275            let metadata = metadata.exec().unwrap();
276
277            let workspace = Workspace {
278                all: true,
279                ..Default::default()
280            };
281            let (included, excluded) = workspace.partition_packages(&metadata);
282            assert_eq!(included.len(), 3);
283            assert_eq!(excluded.len(), 0);
284        }
285
286        #[test]
287        fn pure_ws_root() {
288            let mut metadata = cargo_metadata::MetadataCommand::new();
289            metadata.manifest_path("tests/fixtures/pure_ws/Cargo.toml");
290            let metadata = metadata.exec().unwrap();
291
292            let workspace = Workspace {
293                all: true,
294                ..Default::default()
295            };
296            let (included, excluded) = workspace.partition_packages(&metadata);
297            assert_eq!(included.len(), 3);
298            assert_eq!(excluded.len(), 0);
299        }
300
301        #[test]
302        fn pure_ws_leaf() {
303            let mut metadata = cargo_metadata::MetadataCommand::new();
304            metadata.manifest_path("tests/fixtures/pure_ws/c/Cargo.toml");
305            let metadata = metadata.exec().unwrap();
306
307            let workspace = Workspace {
308                all: true,
309                ..Default::default()
310            };
311            let (included, excluded) = workspace.partition_packages(&metadata);
312            assert_eq!(included.len(), 3);
313            assert_eq!(excluded.len(), 0);
314        }
315    }
316
317    #[cfg(feature = "cargo_metadata")]
318    #[cfg(test)]
319    mod partition_package {
320        use super::*;
321
322        #[test]
323        fn single_crate() {
324            let mut metadata = cargo_metadata::MetadataCommand::new();
325            metadata.manifest_path("tests/fixtures/simple/Cargo.toml");
326            let metadata = metadata.exec().unwrap();
327
328            let workspace = Workspace {
329                package: vec!["simple".to_owned()],
330                ..Default::default()
331            };
332            let (included, excluded) = workspace.partition_packages(&metadata);
333            assert_eq!(included.len(), 1);
334            assert_eq!(excluded.len(), 0);
335        }
336
337        #[test]
338        fn mixed_ws_root() {
339            let mut metadata = cargo_metadata::MetadataCommand::new();
340            metadata.manifest_path("tests/fixtures/mixed_ws/Cargo.toml");
341            let metadata = metadata.exec().unwrap();
342
343            let workspace = Workspace {
344                package: vec!["a".to_owned()],
345                ..Default::default()
346            };
347            let (included, excluded) = workspace.partition_packages(&metadata);
348            assert_eq!(included.len(), 1);
349            assert_eq!(excluded.len(), 2);
350        }
351
352        #[test]
353        fn mixed_ws_leaf() {
354            let mut metadata = cargo_metadata::MetadataCommand::new();
355            metadata.manifest_path("tests/fixtures/mixed_ws/c/Cargo.toml");
356            let metadata = metadata.exec().unwrap();
357
358            let workspace = Workspace {
359                package: vec!["a".to_owned()],
360                ..Default::default()
361            };
362            let (included, excluded) = workspace.partition_packages(&metadata);
363            assert_eq!(included.len(), 1);
364            assert_eq!(excluded.len(), 2);
365        }
366
367        #[test]
368        fn pure_ws_root() {
369            let mut metadata = cargo_metadata::MetadataCommand::new();
370            metadata.manifest_path("tests/fixtures/pure_ws/Cargo.toml");
371            let metadata = metadata.exec().unwrap();
372
373            let workspace = Workspace {
374                package: vec!["a".to_owned()],
375                ..Default::default()
376            };
377            let (included, excluded) = workspace.partition_packages(&metadata);
378            assert_eq!(included.len(), 1);
379            assert_eq!(excluded.len(), 2);
380        }
381
382        #[test]
383        fn pure_ws_leaf() {
384            let mut metadata = cargo_metadata::MetadataCommand::new();
385            metadata.manifest_path("tests/fixtures/pure_ws/c/Cargo.toml");
386            let metadata = metadata.exec().unwrap();
387
388            let workspace = Workspace {
389                package: vec!["a".to_owned()],
390                ..Default::default()
391            };
392            let (included, excluded) = workspace.partition_packages(&metadata);
393            assert_eq!(included.len(), 1);
394            assert_eq!(excluded.len(), 2);
395        }
396    }
397}