1use super::PackageSpec;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "kebab-case")]
18pub struct BuildScope {
19 #[serde(
20 default = "PackageSelection::empty",
21 skip_serializing_if = "PackageSelection::is_empty"
22 )]
23 packages: PackageSelection,
24 #[serde(
25 default = "TargetSelection::empty",
26 skip_serializing_if = "TargetSelection::is_empty"
27 )]
28 targets: TargetSelection,
29 #[serde(
30 default = "FeatureSelection::empty",
31 skip_serializing_if = "FeatureSelection::is_empty"
32 )]
33 features: FeatureSelection,
34}
35
36impl BuildScope {
37 pub fn new(
40 packages: PackageSelection,
41 targets: TargetSelection,
42 features: FeatureSelection,
43 ) -> Self {
44 Self {
45 packages,
46 targets,
47 features,
48 }
49 }
50
51 pub fn packages(&self) -> &PackageSelection {
53 &self.packages
54 }
55
56 pub fn targets(&self) -> &TargetSelection {
58 &self.targets
59 }
60
61 pub fn features(&self) -> &FeatureSelection {
63 &self.features
64 }
65
66 pub fn is_empty(&self) -> bool {
71 let Self {
72 packages,
73 targets,
74 features,
75 } = self;
76 packages.is_empty() && targets.is_empty() && features.is_empty()
77 }
78
79 pub fn cargo_flags(&self) -> Vec<CargoFlag<'_>> {
81 let Self {
82 packages,
83 targets,
84 features,
85 } = self;
86
87 let mut flags = Vec::new();
88 packages.push_cargo_flags(&mut flags);
89 targets.push_cargo_flags(&mut flags);
90 features.push_cargo_flags(&mut flags);
91 flags
92 }
93
94 pub fn to_cargo_args(&self) -> Vec<String> {
99 self.cargo_flags()
100 .into_iter()
101 .map(|flag| flag.to_cargo_arg())
102 .collect()
103 }
104}
105
106impl fmt::Display for BuildScope {
107 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108 for (index, flag) in self.cargo_flags().into_iter().enumerate() {
109 if index > 0 {
110 f.write_str(" ")?;
111 }
112 match flag {
113 CargoFlag::Bare(flag) => f.write_str(flag)?,
114 CargoFlag::Valued { flag, value } => {
115 write!(f, "{flag}={}", shell_words::quote(value))?
119 }
120 }
121 }
122 Ok(())
123 }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
128pub enum CargoFlag<'a> {
129 Bare(&'static str),
131 Valued {
133 flag: &'static str,
135 value: &'a str,
137 },
138}
139
140impl CargoFlag<'_> {
141 pub fn to_cargo_arg(&self) -> String {
143 match self {
144 Self::Bare(flag) => (*flag).to_owned(),
145 Self::Valued { flag, value } => format!("{flag}={value}"),
148 }
149 }
150}
151
152#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
157#[serde(rename_all = "kebab-case")]
158pub struct PackageSelection {
159 #[serde(default, skip_serializing_if = "Vec::is_empty")]
161 pub specs: Vec<PackageSpec>,
162 #[serde(default, skip_serializing_if = "is_false")]
164 pub workspace: bool,
165 #[serde(default, skip_serializing_if = "Vec::is_empty")]
167 pub exclude: Vec<PackageSpec>,
168}
169
170impl PackageSelection {
171 fn empty() -> Self {
172 Self {
173 specs: Vec::new(),
174 workspace: false,
175 exclude: Vec::new(),
176 }
177 }
178
179 pub fn is_empty(&self) -> bool {
181 let Self {
182 specs,
183 workspace,
184 exclude,
185 } = self;
186 specs.is_empty() && !*workspace && exclude.is_empty()
187 }
188
189 fn push_cargo_flags<'a>(&'a self, flags: &mut Vec<CargoFlag<'a>>) {
190 let Self {
191 specs,
192 workspace,
193 exclude,
194 } = self;
195
196 for spec in specs {
197 flags.push(CargoFlag::Valued {
198 flag: "--package",
199 value: spec.as_str(),
200 });
201 }
202 if *workspace {
203 flags.push(CargoFlag::Bare("--workspace"));
204 }
205 for spec in exclude {
206 flags.push(CargoFlag::Valued {
207 flag: "--exclude",
208 value: spec.as_str(),
209 });
210 }
211 }
212}
213
214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
216#[serde(rename_all = "kebab-case")]
217pub struct TargetSelection {
218 #[serde(default, skip_serializing_if = "is_false")]
220 pub lib: bool,
221
222 #[serde(
224 default = "NamedTargets::empty",
225 skip_serializing_if = "NamedTargets::is_empty"
226 )]
227 pub bins: NamedTargets,
228
229 #[serde(
231 default = "NamedTargets::empty",
232 skip_serializing_if = "NamedTargets::is_empty"
233 )]
234 pub examples: NamedTargets,
235
236 #[serde(
238 default = "NamedTargets::empty",
239 skip_serializing_if = "NamedTargets::is_empty"
240 )]
241 pub tests: NamedTargets,
242
243 #[serde(
245 default = "NamedTargets::empty",
246 skip_serializing_if = "NamedTargets::is_empty"
247 )]
248 pub benches: NamedTargets,
249
250 #[serde(default, skip_serializing_if = "is_false")]
252 pub all_targets: bool,
253}
254
255impl TargetSelection {
256 fn empty() -> Self {
257 Self {
258 lib: false,
259 bins: NamedTargets::empty(),
260 examples: NamedTargets::empty(),
261 tests: NamedTargets::empty(),
262 benches: NamedTargets::empty(),
263 all_targets: false,
264 }
265 }
266
267 pub fn is_empty(&self) -> bool {
269 let Self {
270 lib,
271 bins,
272 examples,
273 tests,
274 benches,
275 all_targets,
276 } = self;
277 !*lib
278 && bins.is_empty()
279 && examples.is_empty()
280 && tests.is_empty()
281 && benches.is_empty()
282 && !*all_targets
283 }
284
285 fn push_cargo_flags<'a>(&'a self, flags: &mut Vec<CargoFlag<'a>>) {
286 let Self {
287 lib,
288 bins,
289 examples,
290 tests,
291 benches,
292 all_targets,
293 } = self;
294
295 if *lib {
296 flags.push(CargoFlag::Bare("--lib"));
297 }
298 bins.push_cargo_flags(flags, "--bin", "--bins");
299 examples.push_cargo_flags(flags, "--example", "--examples");
300 tests.push_cargo_flags(flags, "--test", "--tests");
301 benches.push_cargo_flags(flags, "--bench", "--benches");
302 if *all_targets {
303 flags.push(CargoFlag::Bare("--all-targets"));
304 }
305 }
306}
307
308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(rename_all = "kebab-case")]
312pub struct NamedTargets {
313 #[serde(default, skip_serializing_if = "Vec::is_empty")]
315 pub names: Vec<String>,
316
317 #[serde(default, skip_serializing_if = "is_false")]
319 pub all: bool,
320}
321
322impl NamedTargets {
323 pub(crate) fn empty() -> Self {
324 Self {
325 names: Vec::new(),
326 all: false,
327 }
328 }
329
330 pub fn is_empty(&self) -> bool {
332 let Self { names, all } = self;
333 names.is_empty() && !*all
334 }
335
336 fn push_cargo_flags<'a>(
337 &'a self,
338 flags: &mut Vec<CargoFlag<'a>>,
339 name_flag: &'static str,
340 all_flag: &'static str,
341 ) {
342 let Self { names, all } = self;
343
344 for name in names {
345 flags.push(CargoFlag::Valued {
346 flag: name_flag,
347 value: name,
348 });
349 }
350 if *all {
351 flags.push(CargoFlag::Bare(all_flag));
352 }
353 }
354}
355
356#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
358#[serde(rename_all = "kebab-case")]
359pub struct FeatureSelection {
360 #[serde(default, skip_serializing_if = "Vec::is_empty")]
362 pub features: Vec<String>,
363 #[serde(default, skip_serializing_if = "is_false")]
365 pub all_features: bool,
366 #[serde(default, skip_serializing_if = "is_false")]
368 pub no_default_features: bool,
369}
370
371impl FeatureSelection {
372 fn empty() -> Self {
373 Self {
374 features: Vec::new(),
375 all_features: false,
376 no_default_features: false,
377 }
378 }
379
380 pub fn is_empty(&self) -> bool {
382 let Self {
383 features,
384 all_features,
385 no_default_features,
386 } = self;
387 features.is_empty() && !*all_features && !*no_default_features
388 }
389
390 fn push_cargo_flags<'a>(&'a self, flags: &mut Vec<CargoFlag<'a>>) {
391 let Self {
392 features,
393 all_features,
394 no_default_features,
395 } = self;
396
397 for feature in features {
398 flags.push(CargoFlag::Valued {
399 flag: "--features",
400 value: feature,
401 });
402 }
403 if *all_features {
404 flags.push(CargoFlag::Bare("--all-features"));
405 }
406 if *no_default_features {
407 flags.push(CargoFlag::Bare("--no-default-features"));
408 }
409 }
410}
411
412fn is_false(value: &bool) -> bool {
413 !*value
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419 use crate::build_scope::test_helpers::arb_valid_package_spec;
420 use proptest::{collection::vec, prelude::*};
421 use test_strategy::proptest;
422
423 fn spec(input: &str) -> PackageSpec {
424 PackageSpec::new(input).expect("spec is valid")
425 }
426
427 fn named(names: &[&str], all: bool) -> NamedTargets {
428 NamedTargets {
429 names: names.iter().map(|name| (*name).to_owned()).collect(),
430 all,
431 }
432 }
433
434 fn populated_scope() -> BuildScope {
435 BuildScope::new(
436 PackageSelection {
437 specs: vec![spec("foo"), spec("foo@1.2"), spec("nextest-*")],
438 workspace: true,
439 exclude: vec![spec("bar"), spec("baz@0.1.0")],
440 },
441 TargetSelection {
442 lib: true,
443 bins: named(&["my-bin", "bin with space", "back\\slash"], true),
444 examples: named(&["example", ""], true),
445 tests: named(&["integration", "-dash-test", "line\nbreak"], true),
446 benches: named(&["bench"], true),
447 all_targets: true,
448 },
449 FeatureSelection {
450 features: vec![
451 "feat-a".to_owned(),
452 "feat-b feat-c".to_owned(),
453 "-feat-d,feat-e".to_owned(),
454 "it's".to_owned(),
455 ],
456 all_features: true,
457 no_default_features: true,
458 },
459 )
460 }
461
462 fn empty_scope() -> BuildScope {
463 BuildScope::new(
464 PackageSelection::empty(),
465 TargetSelection::empty(),
466 FeatureSelection::empty(),
467 )
468 }
469
470 #[test]
471 fn serialize_populated() {
472 let scope = populated_scope();
473 assert!(!scope.is_empty());
474 let json = serde_json::to_string_pretty(&scope).expect("serialized scope");
475 insta::assert_snapshot!("serialize_populated", json);
476
477 let json = serde_json::to_string_pretty(&empty_scope()).expect("serialized empty scope");
478 insta::assert_snapshot!(json, @"{}");
479
480 let deserialized: BuildScope = serde_json::from_str(
482 r#"{"packages": {"specs": ["foo"], "future": 1}, "future": true}"#,
483 )
484 .expect("scope with unknown fields deserializes");
485 assert_eq!(
486 deserialized,
487 BuildScope::new(
488 PackageSelection {
489 specs: vec![spec("foo")],
490 workspace: false,
491 exclude: Vec::new(),
492 },
493 TargetSelection::empty(),
494 FeatureSelection::empty(),
495 )
496 );
497 }
498
499 #[test]
500 fn cargo_args_and_display() {
501 let scope = populated_scope();
502 let args =
503 serde_json::to_string_pretty(&scope.to_cargo_args()).expect("serialized cargo args");
504 insta::assert_snapshot!("cargo_args_populated", args);
505 insta::assert_snapshot!("display_populated", scope.to_string());
506 }
507
508 fn arb_named_targets() -> impl Strategy<Value = NamedTargets> {
509 let non_empty = (vec(any::<String>(), 0..3), any::<bool>())
510 .prop_map(|(names, all)| NamedTargets { names, all });
511 prop_oneof![1 => Just(NamedTargets::empty()), 2 => non_empty]
512 }
513
514 fn arb_build_scope() -> impl Strategy<Value = BuildScope> {
515 let packages = (
516 vec(arb_valid_package_spec(), 0..3),
517 any::<bool>(),
518 vec(arb_valid_package_spec(), 0..3),
519 )
520 .prop_map(|(specs, workspace, exclude)| PackageSelection {
521 specs,
522 workspace,
523 exclude,
524 });
525 let targets = (
526 any::<bool>(),
527 arb_named_targets(),
528 arb_named_targets(),
529 arb_named_targets(),
530 arb_named_targets(),
531 any::<bool>(),
532 )
533 .prop_map(|(lib, bins, examples, tests, benches, all_targets)| {
534 TargetSelection {
535 lib,
536 bins,
537 examples,
538 tests,
539 benches,
540 all_targets,
541 }
542 });
543 let features = (vec(any::<String>(), 0..3), any::<bool>(), any::<bool>()).prop_map(
544 |(features, all_features, no_default_features)| FeatureSelection {
545 features,
546 all_features,
547 no_default_features,
548 },
549 );
550 (
551 prop_oneof![1 => Just(PackageSelection::empty()), 2 => packages],
552 prop_oneof![1 => Just(TargetSelection::empty()), 2 => targets],
553 prop_oneof![1 => Just(FeatureSelection::empty()), 2 => features],
554 )
555 .prop_map(|(packages, targets, features)| BuildScope::new(packages, targets, features))
556 }
557
558 #[proptest]
559 fn serde_round_trip(#[strategy(arb_build_scope())] scope: BuildScope) {
560 let json = serde_json::to_string(&scope).expect("serialized scope");
561 prop_assert_eq!(scope.is_empty(), json == "{}");
562 let deserialized: BuildScope = serde_json::from_str(&json).expect("deserialized scope");
563 prop_assert_eq!(&deserialized, &scope);
564 prop_assert_eq!(scope.is_empty(), scope.to_cargo_args().is_empty());
565 let split = shell_words::split(&scope.to_string()).expect("display output splits");
566 prop_assert_eq!(split, scope.to_cargo_args());
567 }
568}