1use std::{env, iter};
4
5use cargo_metadata::{camino::Utf8PathBuf, Metadata, Package};
6use clap::ArgAction;
7use eyre::eyre;
8use tracing::Level;
9
10use crate::{
11 workspace::{self, FeatureOption, MetadataExt, PackageExt},
12 Result,
13};
14
15#[derive(Debug, Clone, Default, clap::Args)]
42pub struct Verbosity {
43 #[clap(long, short = 'v', action = ArgAction::Count, global = true)]
45 verbose: u8,
46 #[clap(
48 long,
49 short = 'q',
50 action = ArgAction::Count,
51 global = true,
52 conflicts_with = "verbose"
53 )]
54 quiet: u8,
55}
56
57impl Verbosity {
58 pub fn get(&self) -> Option<Level> {
60 let level = i8::try_from(self.verbose).unwrap_or(i8::MAX)
61 - i8::try_from(self.quiet).unwrap_or(i8::MAX);
62 match level {
63 i8::MIN..=-3 => None,
64 -2 => Some(Level::ERROR),
65 -1 => Some(Level::WARN),
66 0 => Some(Level::INFO),
67 1 => Some(Level::DEBUG),
68 2..=i8::MAX => Some(Level::TRACE),
69 }
70 }
71}
72
73#[derive(Debug, Clone, Default, clap::Args)]
76pub struct EnvArgs {
77 #[clap(
79 long,
80 short = 'e',
81 value_name = "KEY>=<VALUE", value_parser = EnvArgs::parse_parts,
83 )]
84 pub env: Vec<(String, String)>,
85}
86
87impl EnvArgs {
88 pub fn new(iter: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
90 Self {
91 env: iter
92 .into_iter()
93 .map(|(k, v)| (k.into(), v.into()))
94 .collect(),
95 }
96 }
97
98 fn parse_parts(s: &str) -> Result<(String, String)> {
99 match s.split_once('=') {
100 Some((key, value)) => Ok((key.into(), value.into())),
101 None => Ok((s.into(), "".into())),
102 }
103 }
104}
105
106#[derive(Debug, Clone, Default, clap::Args)]
109#[non_exhaustive]
110pub struct WorkspaceArgs {
111 #[clap(long)]
113 pub exhaustive: bool,
114 #[clap(long, conflicts_with = "exhaustive")]
116 pub all_workspaces: bool,
117 #[clap(long)]
119 pub exclude_current_workspace: bool,
120}
121
122impl WorkspaceArgs {
123 pub const EXHAUSTIVE: Self = Self {
125 exhaustive: true,
126 all_workspaces: false,
127 exclude_current_workspace: false,
128 };
129
130 pub fn workspaces(&self) -> impl Iterator<Item = &'static Metadata> {
132 let workspaces = if self.exhaustive || self.all_workspaces {
133 if self.exclude_current_workspace {
134 &workspace::all()[1..]
135 } else {
136 workspace::all()
137 }
138 } else if self.exclude_current_workspace {
139 &workspace::all()[..0]
140 } else {
141 &workspace::all()[..1]
142 };
143 workspaces.iter()
144 }
145}
146
147#[derive(Debug, Clone, Default, clap::Args)]
149#[non_exhaustive]
150pub struct PackageArgs {
151 #[clap(flatten)]
154 pub workspace_args: WorkspaceArgs,
155 #[clap(long, conflicts_with = "exhaustive")]
157 pub workspace: bool,
158 #[clap(long = "package", short = 'p', conflicts_with = "exhaustive")]
160 pub package: Option<String>,
161}
162
163impl PackageArgs {
164 pub const EXHAUSTIVE: Self = Self {
166 workspace_args: WorkspaceArgs::EXHAUSTIVE,
167 workspace: false,
168 package: None,
169 };
170
171 pub fn packages(
173 &self,
174 ) -> impl Iterator<Item = Result<(&'static Metadata, &'static Package)>> + '_ {
175 self.workspace_args
176 .workspaces()
177 .map(move |workspace| {
178 let packages = if self.workspace_args.exhaustive || self.workspace {
179 workspace.workspace_packages()
180 } else if let Some(name) = &self.package {
181 let pkg = workspace
182 .workspace_package_by_name(name)
183 .ok_or_else(|| eyre!("Package not found"))?;
184 vec![pkg]
185 } else {
186 let current_dir = Utf8PathBuf::try_from(env::current_dir()?)?;
187 if let Some(pkg) = workspace.workspace_package_by_path(¤t_dir) {
188 vec![pkg]
189 } else {
190 workspace.workspace_default_packages()
191 }
192 };
193 let it = packages
194 .into_iter()
195 .map(move |package| (workspace, package));
196 Ok(it)
197 })
198 .flat_map(|res| -> Box<dyn Iterator<Item = _>> {
199 match res {
200 Ok(it) => Box::new(it.map(Ok)),
201 Err(err) => Box::new(iter::once(Err(err))),
202 }
203 })
204 }
205}
206
207#[derive(Debug, Clone, Default, clap::Args)]
209#[non_exhaustive]
210pub struct FeatureArgs {
211 #[clap(flatten)]
214 pub package_args: PackageArgs,
215 #[clap(long, conflicts_with = "exhaustive")]
217 pub each_feature: bool,
218}
219
220impl FeatureArgs {
221 pub const EXHAUSTIVE: Self = Self {
223 package_args: PackageArgs::EXHAUSTIVE,
224 each_feature: false,
225 };
226
227 pub fn features(
229 &self,
230 ) -> impl Iterator<
231 Item = Result<(
232 &'static Metadata,
233 &'static Package,
234 Option<FeatureOption<'static>>,
235 )>,
236 > + '_ {
237 self.package_args
238 .packages()
239 .map(move |res| {
240 res.map(move |(workspace, package)| -> Box<dyn Iterator<Item = _>> {
241 let exhaustive = self.package_args.workspace_args.exhaustive;
242 if (exhaustive || self.each_feature) && !package.features.is_empty() {
243 Box::new(
244 package
245 .each_feature()
246 .map(move |feature| (workspace, package, Some(feature))),
247 )
248 } else {
249 Box::new(iter::once((workspace, package, None)))
250 }
251 })
252 })
253 .flat_map(|res| -> Box<dyn Iterator<Item = _>> {
254 match res {
255 Ok(it) => Box::new(it.map(Ok)),
256 Err(err) => Box::new(iter::once(Err(err))),
257 }
258 })
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 #[test]
267 fn verbosity() {
268 use clap::Parser;
269 #[derive(Debug, clap::Parser)]
270 struct App {
271 #[clap(flatten)]
272 verbosity: Verbosity,
273 }
274
275 let cases: &[(&[&str], Option<Level>)] = &[
276 (&["-qqqq"], None),
277 (&["-qqq"], None),
278 (&["-qq"], Some(Level::ERROR)),
279 (&["-q"], Some(Level::WARN)),
280 (&[], Some(Level::INFO)),
281 (&["-v"], Some(Level::DEBUG)),
282 (&["-vv"], Some(Level::TRACE)),
283 ];
284
285 for (arg, level) in cases {
286 let args = App::parse_from(["app"].into_iter().chain(arg.iter().copied()));
287 assert_eq!(args.verbosity.get(), *level, "arg: {}", arg.join(" "));
288 }
289 }
290}