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