#![doc = include_str!("../README.md")]
use core::time::Duration;
use bpaf::{Bpaf, ShellComp};
#[derive(Debug, Clone, Bpaf)]
#[bpaf(generate(cli_global_options))]
#[allow(clippy::upper_case_acronyms)]
pub struct CLIGlobalOptions {
#[bpaf(long("colors"), argument("off|force"))]
pub colors: Option<ColorsArg>,
#[bpaf(short('v'), long("verbose"), switch, fallback(false))]
pub verbose: bool,
#[bpaf(
long("log-level"),
argument("none|debug|info|warn|error"),
fallback(LogLevel::None),
display_fallback
)]
pub log_level: LogLevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorsArg {
Off,
Force,
}
impl core::str::FromStr for ColorsArg {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"off" => Ok(Self::Off),
"force" => Ok(Self::Force),
_ => Err(format!("expected 'off' or 'force', got '{s}'")),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LogLevel {
#[default]
None,
Debug,
Info,
Warn,
Error,
}
impl core::str::FromStr for LogLevel {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"none" => Ok(Self::None),
"debug" => Ok(Self::Debug),
"info" => Ok(Self::Info),
"warn" => Ok(Self::Warn),
"error" => Ok(Self::Error),
_ => Err(format!(
"expected 'none', 'debug', 'info', 'warn', or 'error', got '{s}'"
)),
}
}
}
impl core::fmt::Display for LogLevel {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::Debug => write!(f, "debug"),
Self::Info => write!(f, "info"),
Self::Warn => write!(f, "warn"),
Self::Error => write!(f, "error"),
}
}
}
#[allow(clippy::needless_pass_by_value)] fn parse_duration(s: String) -> Result<Duration, String> {
humantime::parse_duration(&s).map_err(|e| format!("invalid duration '{s}': {e}"))
}
#[derive(Debug, Clone, Bpaf)]
#[bpaf(generate(cli_cache_options))]
#[allow(clippy::struct_excessive_bools)]
pub struct CliCacheOptions {
#[bpaf(long("cache-dir"), argument("DIR"), complete_shell(ShellComp::Dir { mask: None }))]
pub cache_dir: Option<String>,
#[bpaf(long("schema-cache-ttl"), argument::<String>("DURATION"), parse(parse_duration), optional)]
pub schema_cache_ttl: Option<Duration>,
#[bpaf(long("force-schema-fetch"), switch)]
pub force_schema_fetch: bool,
#[bpaf(long("force-validation"), switch)]
pub force_validation: bool,
#[bpaf(long("force"), switch)]
pub force: bool,
#[bpaf(long("no-catalog"), switch)]
pub no_catalog: bool,
}
impl CLIGlobalOptions {
pub fn use_color(&self, is_tty: bool) -> bool {
match self.colors {
Some(ColorsArg::Force) => true,
Some(ColorsArg::Off) => false,
None => is_tty,
}
}
}
pub fn terminal_width() -> usize {
terminal_size::terminal_size()
.map(|(w, _)| w.0 as usize)
.or_else(|| std::env::var("COLUMNS").ok()?.parse().ok())
.unwrap_or(80)
}
pub fn pipe_to_pager(content: &str) {
use std::io::Write;
use std::process::{Command, Stdio};
let pager_env = std::env::var("PAGER").unwrap_or_default();
let (program, args) = if pager_env.is_empty() {
("less", vec!["-R"])
} else {
let mut parts: Vec<&str> = pager_env.split_whitespace().collect();
let prog = parts.remove(0);
if prog == "less" && !parts.iter().any(|a| a.contains('R')) {
parts.push("-R");
}
(prog, parts)
};
match Command::new(program)
.args(&args)
.stdin(Stdio::piped())
.spawn()
{
Ok(mut child) => {
if let Some(mut stdin) = child.stdin.take() {
let _ = write!(stdin, "{content}");
}
let _ = child.wait();
}
Err(_) => {
print!("{content}");
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use bpaf::Parser;
fn opts() -> bpaf::OptionParser<CLIGlobalOptions> {
cli_global_options().to_options()
}
fn cache_opts() -> bpaf::OptionParser<CliCacheOptions> {
cli_cache_options().to_options()
}
#[test]
fn defaults() {
let parsed = opts().run_inner(&[]).unwrap();
assert!(!parsed.verbose);
assert_eq!(parsed.log_level, LogLevel::None);
assert!(parsed.colors.is_none());
}
#[test]
fn verbose_short() {
let parsed = opts().run_inner(&["-v"]).unwrap();
assert!(parsed.verbose);
}
#[test]
fn verbose_long() {
let parsed = opts().run_inner(&["--verbose"]).unwrap();
assert!(parsed.verbose);
}
#[test]
fn log_level_debug() {
let parsed = opts().run_inner(&["--log-level", "debug"]).unwrap();
assert_eq!(parsed.log_level, LogLevel::Debug);
}
#[test]
fn log_level_info() {
let parsed = opts().run_inner(&["--log-level", "info"]).unwrap();
assert_eq!(parsed.log_level, LogLevel::Info);
}
#[test]
fn log_level_warn() {
let parsed = opts().run_inner(&["--log-level", "warn"]).unwrap();
assert_eq!(parsed.log_level, LogLevel::Warn);
}
#[test]
fn log_level_error() {
let parsed = opts().run_inner(&["--log-level", "error"]).unwrap();
assert_eq!(parsed.log_level, LogLevel::Error);
}
#[test]
fn log_level_invalid() {
assert!(opts().run_inner(&["--log-level", "trace"]).is_err());
}
#[test]
fn colors_off() {
let parsed = opts().run_inner(&["--colors", "off"]).unwrap();
assert_eq!(parsed.colors, Some(ColorsArg::Off));
}
#[test]
fn colors_force() {
let parsed = opts().run_inner(&["--colors", "force"]).unwrap();
assert_eq!(parsed.colors, Some(ColorsArg::Force));
}
#[test]
fn colors_invalid() {
assert!(opts().run_inner(&["--colors", "auto"]).is_err());
}
#[test]
fn combined_flags() {
let parsed = opts()
.run_inner(&["-v", "--log-level", "debug", "--colors", "force"])
.unwrap();
assert!(parsed.verbose);
assert_eq!(parsed.log_level, LogLevel::Debug);
assert_eq!(parsed.colors, Some(ColorsArg::Force));
}
#[test]
fn cache_defaults() {
let parsed = cache_opts().run_inner(&[]).unwrap();
assert!(parsed.cache_dir.is_none());
assert!(parsed.schema_cache_ttl.is_none());
assert!(!parsed.force_schema_fetch);
assert!(!parsed.force_validation);
assert!(!parsed.force);
assert!(!parsed.no_catalog);
}
#[test]
fn cache_dir_parsed() {
let parsed = cache_opts()
.run_inner(&["--cache-dir", "/tmp/cache"])
.unwrap();
assert_eq!(parsed.cache_dir.as_deref(), Some("/tmp/cache"));
}
#[test]
fn schema_cache_ttl_parsed() {
let parsed = cache_opts()
.run_inner(&["--schema-cache-ttl", "12h"])
.unwrap();
assert_eq!(
parsed.schema_cache_ttl,
Some(Duration::from_secs(12 * 3600))
);
}
#[test]
fn schema_cache_ttl_invalid() {
assert!(
cache_opts()
.run_inner(&["--schema-cache-ttl", "invalid"])
.is_err()
);
}
#[test]
fn force_schema_fetch_flag() {
let parsed = cache_opts().run_inner(&["--force-schema-fetch"]).unwrap();
assert!(parsed.force_schema_fetch);
}
#[test]
fn force_validation_flag() {
let parsed = cache_opts().run_inner(&["--force-validation"]).unwrap();
assert!(parsed.force_validation);
}
#[test]
fn force_flag() {
let parsed = cache_opts().run_inner(&["--force"]).unwrap();
assert!(parsed.force);
}
#[test]
fn no_catalog_flag() {
let parsed = cache_opts().run_inner(&["--no-catalog"]).unwrap();
assert!(parsed.no_catalog);
}
#[test]
fn cache_combined_flags() {
let parsed = cache_opts()
.run_inner(&[
"--cache-dir",
"/tmp/cache",
"--schema-cache-ttl",
"30m",
"--force-schema-fetch",
"--force-validation",
"--no-catalog",
])
.unwrap();
assert_eq!(parsed.cache_dir.as_deref(), Some("/tmp/cache"));
assert_eq!(parsed.schema_cache_ttl, Some(Duration::from_secs(30 * 60)));
assert!(parsed.force_schema_fetch);
assert!(parsed.force_validation);
assert!(parsed.no_catalog);
}
}