use std::{env, iter};
use cargo_metadata::{Metadata, Package, camino::Utf8PathBuf};
use clap::ArgAction;
use eyre::eyre;
use tracing::Level;
use crate::{
Result,
workspace::{self, FeatureOption, MetadataExt, PackageExt},
};
#[derive(Debug, Clone, Default, clap::Args)]
pub struct Verbosity {
#[clap(long, short = 'v', action = ArgAction::Count, global = true)]
verbose: u8,
#[clap(
long,
short = 'q',
action = ArgAction::Count,
global = true,
conflicts_with = "verbose"
)]
quiet: u8,
}
impl Verbosity {
pub fn get(&self) -> Option<Level> {
let level = i8::try_from(self.verbose).unwrap_or(i8::MAX)
- i8::try_from(self.quiet).unwrap_or(i8::MAX);
match level {
i8::MIN..=-3 => None,
-2 => Some(Level::ERROR),
-1 => Some(Level::WARN),
0 => Some(Level::INFO),
1 => Some(Level::DEBUG),
2..=i8::MAX => Some(Level::TRACE),
}
}
}
#[derive(Debug, Clone, Default, clap::Args)]
pub struct EnvArgs {
#[clap(
long,
short = 'e',
value_name = "KEY>=<VALUE", // hack
value_parser = EnvArgs::parse_parts,
)]
pub env: Vec<(String, String)>,
}
impl EnvArgs {
pub fn new(iter: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>) -> Self {
Self {
env: iter
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect(),
}
}
fn parse_parts(s: &str) -> Result<(String, String)> {
match s.split_once('=') {
Some((key, value)) => Ok((key.into(), value.into())),
None => Ok((s.into(), "".into())),
}
}
}
#[derive(Debug, Clone, Default, clap::Args)]
#[non_exhaustive]
pub struct WorkspaceArgs {
#[clap(long)]
pub exhaustive: bool,
#[clap(long, conflicts_with = "exhaustive")]
pub all_workspaces: bool,
#[clap(long)]
pub exclude_current_workspace: bool,
}
impl WorkspaceArgs {
pub const EXHAUSTIVE: Self = Self {
exhaustive: true,
all_workspaces: false,
exclude_current_workspace: false,
};
pub fn workspaces(&self) -> impl Iterator<Item = &'static Metadata> {
let workspaces = if self.exhaustive || self.all_workspaces {
if self.exclude_current_workspace {
&workspace::all()[1..]
} else {
workspace::all()
}
} else if self.exclude_current_workspace {
&workspace::all()[..0]
} else {
&workspace::all()[..1]
};
workspaces.iter()
}
}
#[derive(Debug, Clone, Default, clap::Args)]
#[non_exhaustive]
pub struct PackageArgs {
#[clap(flatten)]
pub workspace_args: WorkspaceArgs,
#[clap(long, conflicts_with = "exhaustive")]
pub workspace: bool,
#[clap(long = "package", short = 'p', conflicts_with = "exhaustive")]
pub package: Option<String>,
}
impl PackageArgs {
pub const EXHAUSTIVE: Self = Self {
workspace_args: WorkspaceArgs::EXHAUSTIVE,
workspace: false,
package: None,
};
pub fn packages(
&self,
) -> impl Iterator<Item = Result<(&'static Metadata, &'static Package)>> + '_ {
self.workspace_args
.workspaces()
.map(move |workspace| {
let packages = if self.workspace_args.exhaustive || self.workspace {
workspace.workspace_packages()
} else if let Some(name) = &self.package {
let pkg = workspace
.workspace_package_by_name(name)
.ok_or_else(|| eyre!("Package not found"))?;
vec![pkg]
} else {
let current_dir = Utf8PathBuf::try_from(env::current_dir()?)?;
if let Some(pkg) = workspace.workspace_package_by_path(¤t_dir) {
vec![pkg]
} else {
workspace.workspace_default_packages()
}
};
let it = packages
.into_iter()
.map(move |package| (workspace, package));
Ok(it)
})
.flat_map(|res| -> Box<dyn Iterator<Item = _>> {
match res {
Ok(it) => Box::new(it.map(Ok)),
Err(err) => Box::new(iter::once(Err(err))),
}
})
}
}
#[derive(Debug, Clone, Default, clap::Args)]
#[non_exhaustive]
pub struct FeatureArgs {
#[clap(flatten)]
pub package_args: PackageArgs,
#[clap(long, conflicts_with = "exhaustive")]
pub each_feature: bool,
}
impl FeatureArgs {
pub const EXHAUSTIVE: Self = Self {
package_args: PackageArgs::EXHAUSTIVE,
each_feature: false,
};
pub fn features(
&self,
) -> impl Iterator<
Item = Result<(
&'static Metadata,
&'static Package,
Option<FeatureOption<'static>>,
)>,
> + '_ {
self.package_args
.packages()
.map(move |res| {
res.map(move |(workspace, package)| -> Box<dyn Iterator<Item = _>> {
let exhaustive = self.package_args.workspace_args.exhaustive;
if (exhaustive || self.each_feature) && !package.features.is_empty() {
Box::new(
package
.each_feature()
.map(move |feature| (workspace, package, Some(feature))),
)
} else {
Box::new(iter::once((workspace, package, None)))
}
})
})
.flat_map(|res| -> Box<dyn Iterator<Item = _>> {
match res {
Ok(it) => Box::new(it.map(Ok)),
Err(err) => Box::new(iter::once(Err(err))),
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verbosity() {
use clap::Parser;
#[derive(Debug, clap::Parser)]
struct App {
#[clap(flatten)]
verbosity: Verbosity,
}
let cases: &[(&[&str], Option<Level>)] = &[
(&["-qqqq"], None),
(&["-qqq"], None),
(&["-qq"], Some(Level::ERROR)),
(&["-q"], Some(Level::WARN)),
(&[], Some(Level::INFO)),
(&["-v"], Some(Level::DEBUG)),
(&["-vv"], Some(Level::TRACE)),
];
for (arg, level) in cases {
let args = App::parse_from(["app"].into_iter().chain(arg.iter().copied()));
assert_eq!(args.verbosity.get(), *level, "arg: {}", arg.join(" "));
}
}
}