1use clap::{Parser, ValueEnum};
7use color_eyre::eyre::{Result, WrapErr, ensure, eyre};
8use guppy::{
9 PackageId,
10 graph::{DependencyDirection, DependencyReq, PackageGraph, PackageLink, PackageQuery},
11 platform::EnabledTernary,
12};
13use guppy_cmdlib::string_to_platform_spec;
14use std::collections::HashSet;
15
16#[derive(ValueEnum, Copy, Clone, Debug)]
17pub enum Kind {
18 All,
19 Workspace,
20 DirectThirdParty,
21 ThirdParty,
22}
23
24impl Kind {
25 pub fn should_traverse(self, link: &PackageLink<'_>) -> bool {
27 match self {
30 Kind::All | Kind::ThirdParty => true,
31 Kind::DirectThirdParty => link.from().in_workspace(),
32 Kind::Workspace => link.from().in_workspace() && link.to().in_workspace(),
33 }
34 }
35}
36
37#[derive(Debug, Parser)]
38pub struct QueryOptions {
39 #[clap(long = "query-reverse", action = clap::ArgAction::SetTrue)]
41 reverse: bool,
42
43 #[clap(rename_all = "screaming_snake_case")]
44 roots: Vec<String>,
46}
47
48impl QueryOptions {
49 fn direction(&self) -> DependencyDirection {
50 if self.reverse {
51 DependencyDirection::Reverse
52 } else {
53 DependencyDirection::Forward
54 }
55 }
56
57 pub fn apply<'g>(&self, pkg_graph: &'g PackageGraph) -> Result<PackageQuery<'g>> {
59 if !self.roots.is_empty() {
60 let root_set = self.roots.iter().map(|s| s.as_str()).collect();
65 Ok(pkg_graph.query_directed(names_to_ids(pkg_graph, root_set), self.direction())?)
66 } else {
67 ensure!(
68 self.direction() == DependencyDirection::Forward,
69 eyre!("--query-reverse requires roots to be specified")
70 );
71 Ok(pkg_graph.query_workspace())
72 }
73 }
74}
75
76#[derive(Debug, Parser)]
77pub struct BaseFilterOptions {
78 #[clap(long, rename_all = "kebab-case", name = "package")]
79 pub omit_edges_into: Vec<String>,
82
83 #[clap(long, short, value_enum, default_value = "all")]
84 pub kind: Kind,
86}
87
88impl BaseFilterOptions {
89 pub fn omitted_package_ids<'g: 'a, 'a>(
91 &'a self,
92 pkg_graph: &'g PackageGraph,
93 ) -> impl Iterator<Item = &'g PackageId> + 'a {
94 let omitted_set: HashSet<&str> = self.omit_edges_into.iter().map(|s| s.as_str()).collect();
95 names_to_ids(pkg_graph, omitted_set)
96 }
97}
98
99#[derive(Debug, Parser)]
100pub struct FilterOptions {
101 #[clap(flatten)]
102 pub base_opts: BaseFilterOptions,
103
104 #[clap(long, rename_all = "kebab-case")]
105 pub include_dev: bool,
107
108 #[clap(long, rename_all = "kebab-case")]
109 pub include_build: bool,
111
112 #[clap(long)]
113 pub target: Option<String>,
115}
116
117impl FilterOptions {
118 pub fn make_resolver<'g>(
120 &'g self,
121 pkg_graph: &'g PackageGraph,
122 ) -> Result<impl Fn(&PackageQuery<'g>, PackageLink<'g>) -> bool + 'g> {
123 let omitted_package_ids: HashSet<_> =
124 self.base_opts.omitted_package_ids(pkg_graph).collect();
125
126 let platform_spec = string_to_platform_spec(self.target.as_deref())
127 .wrap_err_with(|| "target platform isn't known")?;
128
129 let ret = move |_: &PackageQuery<'g>, link| {
130 let include_kind = self.base_opts.kind.should_traverse(&link);
132
133 let include_type = self.eval(link, |req| {
134 req.status().enabled_on(&platform_spec.clone()) != EnabledTernary::Disabled
135 });
136
137 let include_edge = !omitted_package_ids.contains(link.to().id());
139
140 include_kind && include_type && include_edge
141 };
142 Ok(ret)
143 }
144
145 fn eval(
148 &self,
149 link: PackageLink<'_>,
150 mut pred_fn: impl FnMut(DependencyReq<'_>) -> bool,
151 ) -> bool {
152 pred_fn(link.normal())
153 || self.include_dev && pred_fn(link.dev())
154 || self.include_build && pred_fn(link.build())
155 }
156}
157
158pub(crate) fn names_to_ids<'g: 'a, 'a>(
159 pkg_graph: &'g PackageGraph,
160 names: HashSet<&'a str>,
161) -> impl Iterator<Item = &'g PackageId> + 'a {
162 pkg_graph.packages().filter_map(move |metadata| {
163 if names.contains(metadata.name()) {
164 Some(metadata.id())
165 } else {
166 None
167 }
168 })
169}