1use clap::{ArgEnum, Parser};
7use color_eyre::eyre::{ensure, eyre, Result, WrapErr};
8use guppy::{
9 graph::{DependencyDirection, DependencyReq, PackageGraph, PackageLink, PackageQuery},
10 platform::EnabledTernary,
11 PackageId,
12};
13use guppy_cmdlib::string_to_platform_spec;
14use std::collections::HashSet;
15
16#[derive(ArgEnum, 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", parse(from_flag = parse_direction))]
41 direction: DependencyDirection,
42
43 #[clap(rename_all = "screaming_snake_case")]
44 roots: Vec<String>,
46}
47
48impl QueryOptions {
49 pub fn apply<'g>(&self, pkg_graph: &'g PackageGraph) -> Result<PackageQuery<'g>> {
51 if !self.roots.is_empty() {
52 let root_set = self.roots.iter().map(|s| s.as_str()).collect();
57 Ok(pkg_graph.query_directed(names_to_ids(pkg_graph, root_set), self.direction)?)
58 } else {
59 ensure!(
60 self.direction == DependencyDirection::Forward,
61 eyre!("--query-reverse requires roots to be specified")
62 );
63 Ok(pkg_graph.query_workspace())
64 }
65 }
66}
67
68#[derive(Debug, Parser)]
69pub struct BaseFilterOptions {
70 #[clap(long, rename_all = "kebab-case", name = "package")]
71 pub omit_edges_into: Vec<String>,
74
75 #[clap(long, short, arg_enum, default_value = "all")]
76 pub kind: Kind,
78}
79
80impl BaseFilterOptions {
81 pub fn omitted_package_ids<'g: 'a, 'a>(
83 &'a self,
84 pkg_graph: &'g PackageGraph,
85 ) -> impl Iterator<Item = &'g PackageId> + 'a {
86 let omitted_set: HashSet<&str> = self.omit_edges_into.iter().map(|s| s.as_str()).collect();
87 names_to_ids(pkg_graph, omitted_set)
88 }
89}
90
91#[derive(Debug, Parser)]
92pub struct FilterOptions {
93 #[clap(flatten)]
94 pub base_opts: BaseFilterOptions,
95
96 #[clap(long, rename_all = "kebab-case")]
97 pub include_dev: bool,
99
100 #[clap(long, rename_all = "kebab-case")]
101 pub include_build: bool,
103
104 #[clap(long)]
105 pub target: Option<String>,
107}
108
109impl FilterOptions {
110 pub fn make_resolver<'g>(
112 &'g self,
113 pkg_graph: &'g PackageGraph,
114 ) -> Result<impl Fn(&PackageQuery<'g>, PackageLink<'g>) -> bool + 'g> {
115 let omitted_package_ids: HashSet<_> =
116 self.base_opts.omitted_package_ids(pkg_graph).collect();
117
118 let platform_spec = string_to_platform_spec(self.target.as_deref())
119 .wrap_err_with(|| "target platform isn't known")?;
120
121 let ret = move |_: &PackageQuery<'g>, link| {
122 let include_kind = self.base_opts.kind.should_traverse(&link);
124
125 let include_type = self.eval(link, |req| {
126 req.status().enabled_on(&platform_spec.clone()) != EnabledTernary::Disabled
127 });
128
129 let include_edge = !omitted_package_ids.contains(link.to().id());
131
132 include_kind && include_type && include_edge
133 };
134 Ok(ret)
135 }
136
137 fn eval(
140 &self,
141 link: PackageLink<'_>,
142 mut pred_fn: impl FnMut(DependencyReq<'_>) -> bool,
143 ) -> bool {
144 pred_fn(link.normal())
145 || self.include_dev && pred_fn(link.dev())
146 || self.include_build && pred_fn(link.build())
147 }
148}
149
150pub(crate) fn parse_direction(reverse: bool) -> DependencyDirection {
151 if reverse {
152 DependencyDirection::Reverse
153 } else {
154 DependencyDirection::Forward
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}