Skip to main content

ib_update/github/
blocking.rs

1use super::update_config_builder as builder;
2use crate::github::{Error, Release, UpdateConfig, UpdateConfigBuilder, UpdateInfo};
3use crate::http::BlockingHttp;
4
5/// Synchronous update checker for configuring and executing an update check against a GitHub repo.
6///
7/// Uses [`nyquest::BlockingClient`] directly — no async runtime or
8/// [`futures::executor::block_on`] involved.
9///
10/// # Example
11/// ```no_run
12/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
13/// nyquest_preset::register();
14/// let checker = ib_update::github::UpdateConfig::builder()
15///     .owner("owner")
16///     .repo("repo")
17///     .current_version("1.0.0")
18///     .token("ghp_...")
19///     .build_blocking();
20/// let info = checker.check()?;
21/// println!("Latest: {}, update available: {}", info.latest().tag, info.has_update());
22/// # Ok(())
23/// # }
24/// ```
25pub struct UpdateChecker {
26    inner: UpdateConfig,
27}
28
29impl UpdateChecker {
30    /// Create a `UpdateChecker` from a built [`UpdateConfig`].
31    pub fn new(config: UpdateConfig) -> Self {
32        Self { inner: config }
33    }
34
35    /// Fetch the latest release from GitHub.
36    pub fn fetch_latest(&self) -> Result<Release, Error> {
37        let res = BlockingHttp::get(
38            self.inner.client_builder(),
39            self.inner.releases_latest_path(),
40        )?;
41
42        let status = res.status();
43        let body = res.bytes()?;
44        Release::parse(status, body)
45    }
46
47    /// Fetch up to `n` releases (including pre-releases) from GitHub.
48    ///
49    /// - `n`: Must be <= 100 at the moment.
50    pub fn fetch(&self, n: usize) -> Result<Vec<Release>, Error> {
51        debug_assert!(n <= 100);
52        let res = BlockingHttp::get(
53            self.inner.client_builder(),
54            self.inner.releases_path(n as u8),
55        )?;
56
57        let status = res.status();
58        let body = res.bytes()?;
59        Release::parse_array(status, body)
60    }
61
62    /// Fetch the latest release, including pre-releases.
63    pub fn fetch_latest_prerelease(&self) -> Result<Release, Error> {
64        let releases = self.fetch(1)?;
65        releases.into_iter().next().ok_or(Error::NO_RELEASE)
66    }
67
68    /// Execute the update check.
69    ///
70    /// Fetches the latest release from GitHub and compares it against
71    /// the current version (if set).
72    pub fn check(&self) -> Result<UpdateInfo, Error> {
73        self.check_prerelease(false)
74    }
75
76    /// Execute the update check, optionally including pre-releases.
77    ///
78    /// You likely want to use `!env!("CARGO_PKG_VERSION_PRE").is_empty()` as the default value.
79    pub fn check_prerelease(&self, prerelease: bool) -> Result<UpdateInfo, Error> {
80        let release = if prerelease {
81            self.fetch_latest_prerelease()
82        } else {
83            self.fetch_latest()
84        }?;
85        Ok(UpdateInfo::new(
86            self.inner.current_version.as_deref(),
87            release,
88        ))
89    }
90}
91
92impl<S: builder::State> UpdateConfigBuilder<S> {
93    /// Build the configuration and return an [`UpdateChecker`].
94    ///
95    /// This is a convenience method that combines [`UpdateConfigBuilder::build`](UpdateConfigBuilder::build)
96    /// with [`UpdateChecker::new`].
97    ///
98    /// ```no_run
99    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
100    /// nyquest_preset::register();
101    /// let checker = ib_update::github::UpdateConfig::builder()
102    ///     .owner("owner")
103    ///     .repo("repo")
104    ///     .current_version("1.0.0")
105    ///     .build_blocking();
106    /// let info = checker.check()?;
107    /// println!("Latest: {}, update available: {}", info.latest().tag, info.has_update());
108    /// # Ok(())
109    /// # }
110    /// ```
111    pub fn build_blocking(self) -> UpdateChecker
112    where
113        S: builder::IsComplete,
114    {
115        let config = self.build();
116        UpdateChecker::new(config)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::github::tests::{TEST_OWNER, TEST_REPO};
124
125    #[test]
126    fn fetch_latest() {
127        let config = UpdateConfig::builder()
128            .owner(TEST_OWNER)
129            .repo(TEST_REPO)
130            .build();
131        let release = UpdateChecker::new(config).fetch_latest().unwrap();
132
133        assert!(release.tag.starts_with('v'));
134        assert!(!release.assets.is_empty());
135    }
136
137    #[test]
138    fn fetch() {
139        let config = UpdateConfig::builder()
140            .owner(TEST_OWNER)
141            .repo(TEST_REPO)
142            .build();
143        let releases = UpdateChecker::new(config).fetch(5).unwrap();
144
145        assert!(!releases.is_empty(), "should have at least one release");
146
147        for release in &releases {
148            assert!(!release.tag.is_empty());
149        }
150    }
151
152    #[test]
153    fn fetch_latest_prerelease() {
154        let config = UpdateConfig::builder()
155            .owner(TEST_OWNER)
156            .repo(TEST_REPO)
157            .build();
158        let release = UpdateChecker::new(config)
159            .fetch_latest_prerelease()
160            .unwrap();
161
162        assert!(!release.tag.is_empty());
163    }
164
165    #[test]
166    fn check_has_update() {
167        let info = UpdateConfig::builder()
168            .owner(TEST_OWNER)
169            .repo(TEST_REPO)
170            .current_version("0.1.0")
171            .build_blocking()
172            .check()
173            .unwrap();
174
175        assert!(info.has_update());
176        assert_eq!(info.current_version(), Some("0.1.0"));
177    }
178
179    #[test]
180    fn current_version() {
181        let latest = UpdateConfig::builder()
182            .owner(TEST_OWNER)
183            .repo(TEST_REPO)
184            .build_blocking()
185            .fetch_latest()
186            .unwrap();
187
188        let info = UpdateConfig::builder()
189            .owner(TEST_OWNER)
190            .repo(TEST_REPO)
191            .current_version(&latest.tag)
192            .build_blocking()
193            .check()
194            .unwrap();
195
196        assert!(
197            !info.has_update(),
198            "checking against the latest tag ({}) should not need an update",
199            latest.tag
200        );
201    }
202}