Skip to main content

ib_update/github/
async.rs

1use super::update_config_builder as builder;
2use crate::github::{Error, Release, UpdateConfig, UpdateConfigBuilder, UpdateInfo};
3use crate::http::AsyncHttp;
4
5/// Async builder for configuring and executing an update check against a GitHub repo.
6///
7/// # Example
8/// ```no_run
9/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
10/// nyquest_preset::register();
11/// futures::executor::block_on(async {
12///     let checker = ib_update::github::UpdateConfig::builder()
13///         .owner("owner")
14///         .repo("repo")
15///         .current_version("1.0.0")
16///         .token("ghp_...")
17///         .build_async();
18///     let info = checker.check().await?;
19///     println!("Latest: {}, update available: {}", info.latest().tag, info.has_update());
20///     Ok(())
21/// })
22/// # }
23/// ```
24pub struct AsyncUpdateChecker {
25    inner: UpdateConfig,
26}
27
28impl AsyncUpdateChecker {
29    /// Create an `AsyncUpdateChecker` from a built [`UpdateConfig`].
30    pub fn new(config: UpdateConfig) -> Self {
31        Self { inner: config }
32    }
33
34    /// Fetch the latest release from GitHub.
35    pub async fn fetch_latest(&self) -> Result<Release, Error> {
36        let res = AsyncHttp::get(
37            self.inner.client_builder(),
38            self.inner.releases_latest_path(),
39        )
40        .await?;
41
42        let status = res.status();
43        let body = res.bytes().await?;
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 async fn fetch(&self, n: usize) -> Result<Vec<Release>, Error> {
51        debug_assert!(n <= 100);
52        let res = AsyncHttp::get(
53            self.inner.client_builder(),
54            self.inner.releases_path(n as u8),
55        )
56        .await?;
57
58        let status = res.status();
59        let body = res.bytes().await?;
60        Release::parse_array(status, body)
61    }
62
63    /// Fetch the latest release, including pre-releases.
64    pub async fn fetch_latest_prerelease(&self) -> Result<Release, Error> {
65        let releases = self.fetch(1).await?;
66        releases.into_iter().next().ok_or(Error::NO_RELEASE)
67    }
68
69    /// Execute the update check.
70    ///
71    /// Fetches the latest release from GitHub and compares it against
72    /// the current version (if set).
73    pub async fn check(&self) -> Result<UpdateInfo, Error> {
74        // TODO: rustc can't optimize this yet even with #[inline(always)]
75        // self.check_prerelease(false).await
76        let release = self.fetch_latest().await?;
77        Ok(UpdateInfo::new(
78            self.inner.current_version.as_deref(),
79            release,
80        ))
81    }
82
83    /// Execute the update check, optionally including pre-releases.
84    ///
85    /// You likely want to use `!env!("CARGO_PKG_VERSION_PRE").is_empty()` as the default value.
86    pub async fn check_prerelease(&self, prerelease: bool) -> Result<UpdateInfo, Error> {
87        let release = if prerelease {
88            self.fetch_latest_prerelease().await
89        } else {
90            self.fetch_latest().await
91        }?;
92        Ok(UpdateInfo::new(
93            self.inner.current_version.as_deref(),
94            release,
95        ))
96    }
97}
98
99impl<S: builder::State> UpdateConfigBuilder<S> {
100    /// Build the configuration and return an [`AsyncUpdateChecker`].
101    ///
102    /// This is a convenience method that combines [`UpdateConfigBuilder::build`](UpdateConfigBuilder::build)
103    /// with [`AsyncUpdateChecker::new`].
104    ///
105    /// ```no_run
106    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
107    /// nyquest_preset::register();
108    /// futures::executor::block_on(async {
109    ///     let checker = ib_update::github::UpdateConfig::builder()
110    ///         .owner("owner")
111    ///         .repo("repo")
112    ///         .current_version("1.0.0")
113    ///         .build_async();
114    ///     let info = checker.check().await?;
115    ///     println!("Latest: {}, update available: {}", info.latest().tag, info.has_update());
116    ///     Ok(())
117    /// })
118    /// # }
119    /// ```
120    pub fn build_async(self) -> AsyncUpdateChecker
121    where
122        S: builder::IsComplete,
123    {
124        let config = self.build();
125        AsyncUpdateChecker::new(config)
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::github::tests::{TEST_OWNER, TEST_REPO};
133
134    #[test]
135    fn builder_and_check() {
136        futures::executor::block_on(async {
137            let info = UpdateConfig::builder()
138                .owner(TEST_OWNER)
139                .repo(TEST_REPO)
140                .current_version("0.1.0")
141                .build_async()
142                .check()
143                .await
144                .unwrap();
145            dbg!(&info);
146            assert!(info.has_update());
147            assert!(!info.latest().assets.is_empty());
148        });
149    }
150
151    #[test]
152    fn fetch_latest() {
153        futures::executor::block_on(async {
154            let release = UpdateConfig::builder()
155                .owner(TEST_OWNER)
156                .repo(TEST_REPO)
157                .build_async()
158                .fetch_latest()
159                .await
160                .unwrap();
161
162            assert!(
163                release.tag.starts_with('v'),
164                "tag should start with 'v', got: {}",
165                release.tag
166            );
167            assert!(release.name.is_some(), "release name should be Some");
168            assert!(!release.name.as_ref().unwrap().is_empty());
169            assert!(!release.assets.is_empty(), "release should have assets");
170
171            for asset in &release.assets {
172                assert!(!asset.name.is_empty());
173                assert!(
174                    asset.download_url.starts_with("https://"),
175                    "download URL should be HTTPS: {}",
176                    asset.download_url
177                );
178            }
179
180            assert!(
181                release.html_url.contains("Chaoses-Ib/IbEverythingExt"),
182                "html_url should contain the repo path"
183            );
184            assert!(release.body.is_some(), "release body should be Some");
185            assert!(!release.body.as_ref().unwrap().is_empty());
186        });
187    }
188
189    #[test]
190    fn fetch() {
191        futures::executor::block_on(async {
192            let releases = UpdateConfig::builder()
193                .owner(TEST_OWNER)
194                .repo(TEST_REPO)
195                .build_async()
196                .fetch(5)
197                .await
198                .unwrap();
199
200            assert!(!releases.is_empty(), "should have at least one release");
201
202            for release in &releases {
203                assert!(!release.tag.is_empty());
204                assert!(release.html_url.contains("Chaoses-Ib/IbEverythingExt"));
205            }
206        });
207    }
208
209    #[test]
210    fn fetch_latest_prerelease() {
211        futures::executor::block_on(async {
212            let release = UpdateConfig::builder()
213                .owner(TEST_OWNER)
214                .repo(TEST_REPO)
215                .build_async()
216                .fetch_latest_prerelease()
217                .await
218                .unwrap();
219
220            assert!(!release.tag.is_empty());
221            assert!(release.html_url.contains("Chaoses-Ib/IbEverythingExt"));
222        });
223    }
224
225    #[test]
226    fn current_version() {
227        futures::executor::block_on(async {
228            let latest = UpdateConfig::builder()
229                .owner(TEST_OWNER)
230                .repo(TEST_REPO)
231                .build_async()
232                .fetch_latest()
233                .await
234                .unwrap();
235
236            let info = UpdateConfig::builder()
237                .owner(TEST_OWNER)
238                .repo(TEST_REPO)
239                .current_version(&latest.tag)
240                .build_async()
241                .check()
242                .await
243                .unwrap();
244
245            assert!(
246                !info.has_update(),
247                "checking against the latest tag ({}) should not need an update",
248                latest.tag
249            );
250        });
251    }
252}