cargo-tuwui 0.2.0

A TUI for editing cargo manifest (Cargo.toml) files
use std::fmt::Debug;

use error_stack::{Report, ResultExt};
use serde::Deserialize;
use tracing::{debug, trace};

use crate::{
    fields,
    manifest::dependency::RegistrySource,
    updater::{CheckForUpdateError, UpdateResult},
};

#[derive(Debug, Clone)]
pub struct Updater {
    client: reqwest::Client,
}

impl Updater {
    pub const fn new(client: reqwest::Client) -> Self {
        Self { client }
    }

    #[tracing::instrument(skip(self), fields(url))]
    pub async fn get_versions(
        &self,
        name: &str,
    ) -> Result<Vec<CrateVersion>, Report<CheckForUpdateError>> {
        let url = format!("https://crates.io/api/v1/crates/{name}/versions");

        fields!(url = &url);

        let response = self
            .client
            .get(&url)
            .send()
            .await
            .change_context(CheckForUpdateError::RequestFailed)?
            .json::<CratesIoVersionsResponse>()
            .await
            .change_context(CheckForUpdateError::DeserializationFailed)?;

        Ok(response.versions)
    }

    #[tracing::instrument(skip(versions, version), fields(current_version = %version, version))]
    pub fn filter_update_results<'a>(
        version: &RegistrySource,
        versions: impl Iterator<Item = &'a CrateVersion> + Debug,
    ) -> Result<UpdateResult<semver::Version>, Report<CheckForUpdateError>> {
        let mut latest_version = None;
        let mut semantic_latest_version = None;

        let compatibility_requirement = version.compatible_req();

        for v in versions {
            trace!(version = %&v.num, "Checking compatibility for version");
            if v.yanked {
                trace!(version = %&v.num, "Version is yanked");
                continue;
            }

            if let Some(ref current) = latest_version
                && v.num <= *current
            {
                trace!(version = %&v.num, "Version is not newer than current latest");
                continue;
            }

            debug!(version = %&v.num, "Version is newer than current latest");
            latest_version = Some(v.num.clone());

            if compatibility_requirement.matches(&v.num) {
                if semantic_latest_version
                    .as_ref()
                    .is_some_and(|c| &v.num <= c)
                {
                    trace!(version = %&v.num, "Version is not newer than current semantic latest");
                    continue;
                }

                debug!(version = %&v.num, "Version is compatible and newer than current semantic latest");
                semantic_latest_version = Some(v.num.clone());
            }
        }

        debug!(latest_version = ?latest_version, semantic_latest_version = ?semantic_latest_version, "Filtered update results");

        Ok(UpdateResult {
            latest_version,
            semantic_latest_version,
        })
    }
}

#[derive(Debug, Deserialize)]
struct CratesIoVersionsResponse {
    pub versions: Vec<CrateVersion>,
}

#[derive(Debug, Deserialize)]
pub struct CrateVersion {
    pub num: semver::Version,
    pub yanked: bool,
}