use std::io::Write;
use clap::Parser;
use owo_colors::OwoColorize;
use ingredients::{Crate, Diff, Report, ReportOptions, Severity};
fn pretty_format_severity(severity: Severity) -> String {
match severity {
x @ Severity::Fatal => x.to_string().bold().on_bright_cyan().to_string(),
x @ Severity::Error => x.to_string().red().bold().to_string(),
x @ Severity::Warning => x.to_string().yellow().bold().to_string(),
x @ Severity::Debug => x.to_string().green().bold().to_string(),
x @ Severity::Info | x => x.bold().to_string(),
}
}
const fn plural_suffix(len: usize) -> &'static str {
if len == 1 { "" } else { "s" }
}
fn pretty_print_report(report: &Report, minimum: Severity, verbose: bool) {
let mut out = anstream::stdout();
for item in report.items() {
if item.severity() < minimum {
continue;
}
let severity = pretty_format_severity(item.severity());
if verbose {
let _ = writeln!(out, "{}: {}", severity, item.message());
if let Some(extra) = item.extra() {
for line in extra.lines() {
let _ = writeln!(out, " {line}");
}
let _ = writeln!(out);
}
} else {
let _ = writeln!(out, "{}: {}", severity, item.message());
}
}
let fatal = report
.items()
.iter()
.filter(|item| item.severity() == Severity::Fatal)
.collect::<Vec<_>>()
.len();
let error = report
.items()
.iter()
.filter(|item| item.severity() == Severity::Error)
.collect::<Vec<_>>()
.len();
let warning = report
.items()
.iter()
.filter(|item| item.severity() == Severity::Warning)
.collect::<Vec<_>>()
.len();
let _ = writeln!(
out,
"{}: {} fatal issue{}, {} error{}, {} warning{}",
"summary".bold(),
fatal,
plural_suffix(fatal),
error,
plural_suffix(error),
warning,
plural_suffix(warning),
);
}
fn pretty_print_diff(diff: &Diff, minimum: Severity, verbose: bool) {
let mut out = anstream::stdout();
for item in diff.items() {
if item.severity() < minimum {
continue;
}
let severity = pretty_format_severity(item.severity());
if verbose {
let _ = writeln!(out, "{}: {}", severity, item.message());
if let Some(extra) = item.extra() {
for line in extra.lines() {
let _ = writeln!(out, " {line}");
}
let _ = writeln!(out);
}
} else {
let _ = writeln!(out, "{}: {}", severity, item.message());
}
}
let fatal = diff
.items()
.iter()
.filter(|item| item.severity() == Severity::Fatal)
.collect::<Vec<_>>()
.len();
let error = diff
.items()
.iter()
.filter(|item| item.severity() == Severity::Error)
.collect::<Vec<_>>()
.len();
let warning = diff
.items()
.iter()
.filter(|item| item.severity() == Severity::Warning)
.collect::<Vec<_>>()
.len();
let _ = writeln!(
out,
"{}: {} fatal issue{}, {} error{}, {} warning{}",
"summary".bold(),
fatal,
plural_suffix(fatal),
error,
plural_suffix(error),
warning,
plural_suffix(warning),
);
}
#[derive(Clone, Copy, Debug, Default, clap::ValueEnum)]
enum Format {
#[default]
Plain,
Verbose,
Json,
}
#[derive(Debug, clap::Parser)]
#[command(disable_help_subcommand(true))]
struct Args {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, clap::Args)]
struct ReportArgs {
name: String,
version: String,
#[clap(long, default_value = "plain")]
format: Format,
#[clap(long, default_value = "info")]
severity: Severity,
#[clap(long, short = 'f')]
file: Option<String>,
#[clap(long, short = 'u')]
with_repository: Option<String>,
#[clap(long, short = 'p')]
with_path_in_vcs: Option<String>,
#[clap(long, short = 'r')]
with_vcs_ref: Option<String>,
}
#[derive(Debug, clap::Args)]
struct DiffArgs {
name: String,
old_version: String,
new_version: String,
#[clap(long, default_value = "plain")]
format: Format,
#[clap(long, default_value = "info")]
severity: Severity,
#[clap(long)]
old_file: Option<String>,
#[clap(long)]
new_file: Option<String>,
}
#[derive(Debug, clap::Subcommand)]
enum Command {
Report(ReportArgs),
Diff(DiffArgs),
}
#[expect(clippy::print_stdout)]
async fn report(args: ReportArgs) -> anyhow::Result<()> {
let mut options = ReportOptions::default();
options.repository = args.with_repository;
options.path_in_vcs = args.with_path_in_vcs;
options.vcs_ref = args.with_vcs_ref;
let krate = if let Some(path) = args.file {
Crate::local(&args.name, &args.version, &path)?
} else {
Crate::download(&args.name, &args.version).await?
};
let report = if options.is_empty() {
krate.report().await?
} else {
krate.report_with_options(options).await?
};
match args.format {
Format::Plain => pretty_print_report(&report, args.severity, false),
Format::Verbose => pretty_print_report(&report, args.severity, true),
Format::Json => println!("{}", report.to_json()),
}
Ok(())
}
#[expect(clippy::print_stdout)]
async fn diff(args: DiffArgs) -> anyhow::Result<()> {
let old_crate = if let Some(old_file) = args.old_file {
Crate::local(&args.name, &args.old_version, &old_file)?
} else {
Crate::download(&args.name, &args.old_version).await?
};
let new_crate = if let Some(new_file) = args.new_file {
Crate::local(&args.name, &args.new_version, &new_file)?
} else {
Crate::download(&args.name, &args.new_version).await?
};
let diff = old_crate.diff(&new_crate)?;
match args.format {
Format::Plain => pretty_print_diff(&diff, args.severity, false),
Format::Verbose => pretty_print_diff(&diff, args.severity, true),
Format::Json => println!("{}", diff.to_json()),
}
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
env_logger::init_from_env("INGREDIENTS_LOG");
let args = Args::parse();
match args.command {
Command::Report(args) => report(args).await?,
Command::Diff(args) => diff(args).await?,
}
Ok(())
}