1use serde::Deserialize;
9use thiserror::Error;
10
11#[cfg(all(feature = "github-extra", feature = "multi-server"))]
12use update_config_builder::{IsUnset, SetBaseUrls, State};
13
14#[cfg(feature = "async")]
15mod r#async;
16#[cfg(feature = "blocking")]
17mod blocking;
18
19#[cfg(feature = "async")]
20pub use r#async::AsyncUpdateChecker;
21#[cfg(feature = "blocking")]
22pub use blocking::UpdateChecker;
23
24#[derive(Debug, Error)]
26pub enum Error {
27 #[error(transparent)]
29 Nyquest(#[from] nyquest::Error),
30
31 #[error("rate limit")]
36 RateLimit,
37}
38
39impl Error {
40 pub const NO_RELEASE: Error = Error::Nyquest(nyquest::Error::NonSuccessfulStatusCode(
41 nyquest::StatusCode::new(404),
42 ));
43}
44
45#[derive(Debug, Clone, Deserialize)]
49#[cfg_attr(test, derive(PartialEq))]
50pub struct Release {
51 #[serde(rename = "tag_name")]
53 pub tag: String,
54
55 pub name: Option<String>,
57
58 #[serde(default)]
60 pub prerelease: bool,
61
62 pub created_at: String,
64
65 pub published_at: Option<String>,
67
68 pub body: Option<String>,
70
71 pub html_url: String,
73
74 #[serde(default)]
76 pub assets: Vec<ReleaseAsset>,
77}
78
79impl Release {
80 fn check_status(status: nyquest::StatusCode, body: &[u8]) -> Result<(), Error> {
82 if !status.is_successful() {
83 if status == 403 && body.array_windows::<4>().any(|x| x == b"rate") {
85 return Err(Error::RateLimit);
86 }
87 return Err(Error::Nyquest(nyquest::Error::NonSuccessfulStatusCode(
88 status,
89 )));
90 }
91 Ok(())
92 }
93
94 pub(crate) fn parse(status: nyquest::StatusCode, body: Vec<u8>) -> Result<Release, Error> {
99 Self::check_status(status, &body)?;
100 let release: Release = serde_json::from_slice(&body).map_err(nyquest::Error::from)?;
101 Ok(release)
102 }
103
104 pub(crate) fn parse_array(
106 status: nyquest::StatusCode,
107 body: Vec<u8>,
108 ) -> Result<Vec<Release>, Error> {
109 Self::check_status(status, &body)?;
110 let releases: Vec<Release> = serde_json::from_slice(&body).map_err(nyquest::Error::from)?;
111 Ok(releases)
112 }
113}
114
115#[derive(Debug, Clone, Deserialize)]
117#[cfg_attr(test, derive(PartialEq))]
118pub struct ReleaseAsset {
119 pub name: String,
121
122 #[serde(rename = "browser_download_url")]
124 pub download_url: String,
125
126 pub size: u64,
128
129 pub digest: Option<String>,
131
132 #[cfg(feature = "github-extra")]
136 pub download_count: u64,
137}
138
139#[derive(Debug, Clone)]
141pub struct UpdateInfo {
142 current_version: Option<String>,
144 latest: Release,
146 has_update: bool,
148}
149
150impl UpdateInfo {
151 pub(crate) fn new(current_version: Option<&str>, latest: Release) -> UpdateInfo {
153 let has_update = match current_version {
154 Some(cv) => crate::git::is_newer(cv, &latest.tag),
155 None => true,
156 };
157
158 UpdateInfo {
159 current_version: current_version.map(str::to_string),
160 latest,
161 has_update,
162 }
163 }
164
165 pub fn current_version(&self) -> Option<&str> {
167 self.current_version.as_deref()
168 }
169
170 pub fn latest(&self) -> &Release {
172 &self.latest
173 }
174
175 #[doc(alias = "update_available")]
182 #[inline]
183 pub fn has_update(&self) -> bool {
184 self.has_update
185 }
186}
187
188#[derive(bon::Builder)]
192#[builder(on(String, into))]
193pub struct UpdateConfig {
194 pub(crate) owner: String,
196
197 pub(crate) repo: String,
199
200 pub(crate) current_version: Option<String>,
202
203 #[cfg(feature = "github-extra")]
205 pub(crate) token: Option<String>,
206
207 #[cfg(all(feature = "github-extra", not(feature = "multi-server")))]
209 pub(crate) base_url: Option<String>,
210
211 #[cfg(feature = "multi-server")]
213 #[builder(default = vec!["https://api.github.com".into()])]
215 pub(crate) base_urls: Vec<String>,
216
217 #[cfg(feature = "github-extra")]
219 pub(crate) user_agent: Option<String>,
220}
221
222#[cfg(all(feature = "github-extra", feature = "multi-server"))]
223impl<S: State> UpdateConfigBuilder<S> {
224 pub fn base_url(self, url: String) -> UpdateConfigBuilder<SetBaseUrls<S>>
225 where
226 S::BaseUrls: IsUnset,
227 {
228 self.base_urls(vec![url])
229 }
230
231 pub fn maybe_base_url(self, url: Option<String>) -> UpdateConfigBuilder<SetBaseUrls<S>>
232 where
233 S::BaseUrls: IsUnset,
234 {
235 self.maybe_base_urls(url.map(|s| vec![s]))
236 }
237}
238
239impl UpdateConfig {
240 #[inline(always)]
246 pub(crate) fn client_builder(&self) -> impl Iterator<Item = nyquest::ClientBuilder> {
247 let it = cfg_select! {
248 feature = "multi-server" => self.base_urls.iter(),
249 feature = "github-extra" => {{
250 let base = self.base_url.as_deref().unwrap_or("https://api.github.com");
254 [base].into_iter()
255 }}
256 _ => ["https://api.github.com"].into_iter()
257 };
258
259 let default_ua = concat!("ib-update/", env!("CARGO_PKG_VERSION"));
260 let ua = cfg_select! {
261 feature = "github-extra" => self.user_agent.as_deref().unwrap_or(&default_ua),
263 _ => default_ua,
264 };
265
266 it.map(move |base| {
267 #[allow(unused_mut)]
268 let mut builder = nyquest::ClientBuilder::default()
269 .base_url(base)
270 .user_agent(ua);
271
272 #[cfg(feature = "github-extra")]
274 if let Some(ref token) = self.token {
275 builder = builder.with_header("Authorization", format!("Bearer {token}"));
276 }
277
278 builder
279 })
280 }
281
282 pub(crate) fn releases_latest_path(&self) -> String {
288 format!("repos/{}/{}/releases/latest", self.owner, self.repo)
289 }
290
291 pub(crate) fn releases_path(&self, per_page: u8) -> String {
297 format!(
298 "repos/{}/{}/releases?per_page={per_page}",
299 self.owner, self.repo
300 )
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 pub const TEST_OWNER: &str = "Chaoses-Ib";
308 pub const TEST_REPO: &str = "IbEverythingExt";
310}