ingredients 0.2.0

Check ingredients of published Rust crates
Documentation
#![doc = include_str!("../README.md")]

mod compare;
mod diff;
mod error;
mod format;
mod krate;
mod metadata;
mod report;
mod severity;
mod upstream;
mod utils;
mod vcsinfo;
mod workarounds;

pub use crate::diff::{Diff, DiffItem};
pub use crate::error::Error;
pub use crate::krate::Crate;
pub use crate::report::{Report, ReportItem};
pub use crate::severity::Severity;

use crate::compare::{CrateComparator, RepoComparator};
use crate::upstream::Repository;
use crate::workarounds::sanitize_repo_url;

// part of the public API
#[doc(hidden)]
pub use serde_json::Value;

/// Additional options for report generation
///
/// ```rust
/// use ingredients::ReportOptions;
///
/// let mut options = ReportOptions::default();
/// options.repository = Some(String::from("https://example.com/foo/bar"));
/// ```
#[non_exhaustive]
#[derive(Clone, Default)]
pub struct ReportOptions {
    /// Repository URL
    ///
    /// This is useful if published crates do not contain a correct repository URL.
    pub repository: Option<String>,
    /// Path in VCS
    ///
    /// This is useful for crates that were published with old versions of cargo
    /// that did not include the `"path_in_vcs"` property in `.cargo_vcs_info.json`
    /// files.
    pub path_in_vcs: Option<String>,
    /// VCS ref
    ///
    /// This is useful for crates that were publsihed with old versions of cargo
    /// that did not include a `.cargo_vcs_info.json` file on upload to crates.io.
    pub vcs_ref: Option<String>,
}

impl ReportOptions {
    /// Initialize new options.
    #[must_use]
    pub const fn new() -> Self {
        ReportOptions {
            repository: None,
            path_in_vcs: None,
            vcs_ref: None,
        }
    }

    /// Check if the options are empty / the defaults.
    #[must_use]
    pub const fn is_empty(&self) -> bool {
        self.repository.is_none() && self.path_in_vcs.is_none() && self.vcs_ref.is_none()
    }
}

impl Crate {
    /// Compare crate against the contents of the project's version control system.
    ///
    /// ```rust,no_run
    /// use ingredients::Crate;
    /// # use tokio::runtime::Runtime;
    ///
    /// # let rt = Runtime::new().unwrap();
    /// # rt.block_on(async {
    /// let krate = Crate::download("ingredients", "0.1.0").await.unwrap();
    /// let report = krate.report().await.unwrap();
    /// println!("{report}");
    /// # })
    /// ```
    ///
    /// **Warning**: This attempts to perform a `git clone`, i.e. a git fetch and checkout of the
    /// referenced repository in a temporary directory.
    /// 
    /// # Errors
    ///
    /// * The repository URL is invalid and fails to parse.
    /// * Directory contents cannot be read from disk.
    pub async fn report(&self) -> Result<Report, Error> {
        self.report_with_options(ReportOptions::new()).await
    }

    /// Compare crate against the contents of the project's version control system (with custom
    /// options).
    ///
    /// **Warning**: This attempts to perform a `git clone`, i.e. a git fetch and checkout of the
    /// referenced repository in a temporary directory.
    ///
    /// # Errors
    ///
    /// * The repository URL is invalid and fails to parse.
    /// * Directory contents cannot be read from disk.
    pub async fn report_with_options(&self, options: ReportOptions) -> Result<Report, Error> {
        let mut items = Vec::new();

        let mut vcsinfo_path_in_vcs = None;
        let mut vcsinfo_vcs_ref = None;

        if let Some(vcs_info) = &self.vcs_info {
            if vcs_info.git.dirty {
                items.push(ReportItem::DirtyRepository);
            }

            if vcs_info.path_in_vcs.is_none() {
                items.push(ReportItem::NoPathInVcsInfo);
            }

            vcsinfo_path_in_vcs = vcs_info.path_in_vcs.as_deref();
            vcsinfo_vcs_ref = Some(vcs_info.git.sha1.as_str());
        } else {
            items.push(ReportItem::MissingVcsInfo);
        }

        let Some(vcs_ref) = options.vcs_ref.as_deref().or(vcsinfo_vcs_ref) else {
            return Ok(Report::from_items(items));
        };

        let path_in_vcs = options.path_in_vcs.as_deref().or(vcsinfo_path_in_vcs);

        let Some(url) = options.repository.or(self.metadata.inner.repository.clone()) else {
            items.push(ReportItem::MissingRepositoryUrl);
            return Ok(Report::from_items(items));
        };

        let sanitized_url = sanitize_repo_url(&url)?;
        let upstream = match Repository::clone(&sanitized_url, vcs_ref, path_in_vcs, &self.metadata.inner.name).await {
            Ok(repo) => repo,
            Err(err) => match err {
                Error::InvalidGitRef { repo, rev } => {
                    items.push(ReportItem::InvalidGitRef { repo, rev });
                    return Ok(Report::from_items(items));
                },
                Error::InvalidRepoUrl { repo } => {
                    items.push(ReportItem::InvalidRepoUrl { repo });
                    return Ok(Report::from_items(items));
                },
                Error::CrateNotFound { name } => {
                    items.push(ReportItem::NotFoundInRepo { repo: url, name });
                    return Ok(Report::from_items(items));
                },
                _ => return Err(err),
            },
        };

        let comparator = RepoComparator::new(self, &upstream);
        items.extend(comparator.compare()?);

        Ok(Report::from_items(items))
    }

    /// Compare crate against a different version of the same crate.
    ///
    /// ```rust,no_run
    /// use ingredients::Crate;
    /// # use tokio::runtime::Runtime;
    ///
    /// # let rt = Runtime::new().unwrap();
    /// # rt.block_on(async {
    /// let krate1 = Crate::download("syn", "2.0.110").await.unwrap();
    /// let krate2 = Crate::download("syn", "2.0.111").await.unwrap();
    /// let diff = krate1.diff(&krate2).unwrap();
    /// println!("{diff}");
    /// # })
    /// ```
    ///
    /// # Errors
    ///
    /// * Directory contents cannot be read from disk.
    pub fn diff(&self, other: &Crate) -> Result<Diff, Error> {
        let comparator = CrateComparator::new(self, other);
        Ok(Diff::from_items(comparator.compare()?))
    }
}