ingredients 0.1.1

Check ingredients of published Rust crates
Documentation
//! ingredients CLI

use clap::Parser;

use ingredients::{Crate, ReportOptions};

/// Check ingredients of published Rust crates
#[derive(Debug, clap::Parser)]
#[command(disable_help_subcommand(true))]
struct Args {
    #[command(subcommand)]
    command: Command,
}

/// Compare a crate against the contents of the upstream version control system
///
/// By default, the crate archive is downloaded from crates.io. To override this
/// behaviour, pass `--file <PATH>` to use a locally downloaded file instead.
///
/// For crates that don't set `package.repository` in their metadata, a URL can
/// be provided with the `--with-repository <URL>` argument.
///
/// For crates that have been published with old versions of cargo that did not
/// yet include complete `.cargo_vcs_info.json` files, the "path in VCS" (the
/// relative path where the crate can be found in the repository) can be
/// provided with the `--with-path-in-vcs <PATH>` argument, and the VCS ref can
/// be provided with the `--with-vcs-ref <REF>` argument.
#[derive(Debug, clap::Args)]
struct ReportArgs {
    /// Name of the crate
    name: String,
    /// Version of the crate
    version: String,

    /// Print results in machine-readable JSON format
    #[clap(long)]
    json: bool,
    /// Print verbose results (including diffs)
    #[clap(long, short = 'v')]
    verbose: bool,

    /// Path to local crate archive to use instead of downloading from crates.io
    #[clap(long, short = 'f')]
    file: Option<String>,

    /// Provide repository URL if package.repository is missing or wrong
    #[clap(long, short = 'u')]
    with_repository: Option<String>,
    /// Provide "path in VCS" if `.cargo_vcs_info.json` is missing or incomplete
    #[clap(long, short = 'p')]
    with_path_in_vcs: Option<String>,
    /// Provide VCS ref if `.cargo_vcs_info.json` is missing or incomplete
    #[clap(long, short = 'r')]
    with_vcs_ref: Option<String>,
}

/// Compare two versions of a crate
///
/// By default, the crate archives are downloaded from crates.io. To override
/// this behaviour, pass `--old_file <PATH>` and `--new-file <PATH>` to use
/// locally downloaded files instead.
#[derive(Debug, clap::Args)]
struct DiffArgs {
    /// Name of the crate
    name: String,
    /// Old version of the crate
    old_version: String,
    /// New version of the crate
    new_version: String,

    /// Print results in machine-readable JSON format
    #[clap(long)]
    json: bool,
    /// Print verbose results (including diffs)
    #[clap(long, short = 'v')]
    verbose: bool,

    /// Path to local crate archive for old crate version
    #[clap(long)]
    old_file: Option<String>,
    /// Path to local crate archive for new crate version
    #[clap(long)]
    new_file: Option<String>,
}

#[derive(Debug, clap::Subcommand)]
enum Command {
    Report(ReportArgs),
    Diff(DiffArgs),
}

#[allow(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?
    };

    if args.json {
        println!("{}", report.to_json());
    } else if args.verbose {
        println!("{report:#}");
    } else {
        println!("{report}");
    }

    Ok(())
}

#[allow(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)?;

    if args.json {
        println!("{}", diff.to_json());
    } else if args.verbose {
        println!("{diff:#}");
    } else {
        println!("{diff}");
    }

    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(())
}