ib-update 0.1.0

A lightweight library for software update
Documentation
use super::update_config_builder as builder;
use crate::github::{Error, Release, UpdateConfig, UpdateConfigBuilder, UpdateInfo};
use crate::http::BlockingHttp;

/// Synchronous update checker for configuring and executing an update check against a GitHub repo.
///
/// Uses [`nyquest::BlockingClient`] directly — no async runtime or
/// [`futures::executor::block_on`] involved.
///
/// # Example
/// ```no_run
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
/// nyquest_preset::register();
/// let checker = ib_update::github::UpdateConfig::builder()
///     .owner("owner")
///     .repo("repo")
///     .current_version("1.0.0")
///     .token("ghp_...")
///     .build_blocking();
/// let info = checker.check()?;
/// println!("Latest: {}, update available: {}", info.latest().tag, info.has_update());
/// # Ok(())
/// # }
/// ```
pub struct UpdateChecker {
    inner: UpdateConfig,
}

impl UpdateChecker {
    /// Create a `UpdateChecker` from a built [`UpdateConfig`].
    pub fn new(config: UpdateConfig) -> Self {
        Self { inner: config }
    }

    /// Fetch the latest release from GitHub.
    pub fn fetch_latest(&self) -> Result<Release, Error> {
        let res = BlockingHttp::get(
            self.inner.client_builder(),
            self.inner.releases_latest_path(),
        )?;

        let status = res.status();
        let body = res.bytes()?;
        Release::parse(status, body)
    }

    /// Fetch up to `n` releases (including pre-releases) from GitHub.
    ///
    /// - `n`: Must be <= 100 at the moment.
    pub fn fetch(&self, n: usize) -> Result<Vec<Release>, Error> {
        debug_assert!(n <= 100);
        let res = BlockingHttp::get(
            self.inner.client_builder(),
            self.inner.releases_path(n as u8),
        )?;

        let status = res.status();
        let body = res.bytes()?;
        Release::parse_array(status, body)
    }

    /// Fetch the latest release, including pre-releases.
    pub fn fetch_latest_prerelease(&self) -> Result<Release, Error> {
        let releases = self.fetch(1)?;
        releases.into_iter().next().ok_or(Error::NO_RELEASE)
    }

    /// Execute the update check.
    ///
    /// Fetches the latest release from GitHub and compares it against
    /// the current version (if set).
    pub fn check(&self) -> Result<UpdateInfo, Error> {
        self.check_prerelease(false)
    }

    /// Execute the update check, optionally including pre-releases.
    ///
    /// You likely want to use `!env!("CARGO_PKG_VERSION_PRE").is_empty()` as the default value.
    pub fn check_prerelease(&self, prerelease: bool) -> Result<UpdateInfo, Error> {
        let release = if prerelease {
            self.fetch_latest_prerelease()
        } else {
            self.fetch_latest()
        }?;
        Ok(UpdateInfo::new(
            self.inner.current_version.as_deref(),
            release,
        ))
    }
}

impl<S: builder::State> UpdateConfigBuilder<S> {
    /// Build the configuration and return an [`UpdateChecker`].
    ///
    /// This is a convenience method that combines [`UpdateConfigBuilder::build`](UpdateConfigBuilder::build)
    /// with [`UpdateChecker::new`].
    ///
    /// ```no_run
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// nyquest_preset::register();
    /// let checker = ib_update::github::UpdateConfig::builder()
    ///     .owner("owner")
    ///     .repo("repo")
    ///     .current_version("1.0.0")
    ///     .build_blocking();
    /// let info = checker.check()?;
    /// println!("Latest: {}, update available: {}", info.latest().tag, info.has_update());
    /// # Ok(())
    /// # }
    /// ```
    pub fn build_blocking(self) -> UpdateChecker
    where
        S: builder::IsComplete,
    {
        let config = self.build();
        UpdateChecker::new(config)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::github::tests::{TEST_OWNER, TEST_REPO};

    #[test]
    fn fetch_latest() {
        let config = UpdateConfig::builder()
            .owner(TEST_OWNER)
            .repo(TEST_REPO)
            .build();
        let release = UpdateChecker::new(config).fetch_latest().unwrap();

        assert!(release.tag.starts_with('v'));
        assert!(!release.assets.is_empty());
    }

    #[test]
    fn fetch() {
        let config = UpdateConfig::builder()
            .owner(TEST_OWNER)
            .repo(TEST_REPO)
            .build();
        let releases = UpdateChecker::new(config).fetch(5).unwrap();

        assert!(!releases.is_empty(), "should have at least one release");

        for release in &releases {
            assert!(!release.tag.is_empty());
        }
    }

    #[test]
    fn fetch_latest_prerelease() {
        let config = UpdateConfig::builder()
            .owner(TEST_OWNER)
            .repo(TEST_REPO)
            .build();
        let release = UpdateChecker::new(config)
            .fetch_latest_prerelease()
            .unwrap();

        assert!(!release.tag.is_empty());
    }

    #[test]
    fn check_has_update() {
        let info = UpdateConfig::builder()
            .owner(TEST_OWNER)
            .repo(TEST_REPO)
            .current_version("0.1.0")
            .build_blocking()
            .check()
            .unwrap();

        assert!(info.has_update());
        assert_eq!(info.current_version(), Some("0.1.0"));
    }

    #[test]
    fn current_version() {
        let latest = UpdateConfig::builder()
            .owner(TEST_OWNER)
            .repo(TEST_REPO)
            .build_blocking()
            .fetch_latest()
            .unwrap();

        let info = UpdateConfig::builder()
            .owner(TEST_OWNER)
            .repo(TEST_REPO)
            .current_version(&latest.tag)
            .build_blocking()
            .check()
            .unwrap();

        assert!(
            !info.has_update(),
            "checking against the latest tag ({}) should not need an update",
            latest.tag
        );
    }
}