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