use serde::Deserialize;
use thiserror::Error;
#[cfg(all(feature = "github-extra", feature = "multi-server"))]
use update_config_builder::{IsUnset, SetBaseUrls, State};
#[cfg(feature = "async")]
mod r#async;
#[cfg(feature = "blocking")]
mod blocking;
#[cfg(feature = "async")]
pub use r#async::AsyncUpdateChecker;
#[cfg(feature = "blocking")]
pub use blocking::UpdateChecker;
#[derive(Debug, Error)]
pub enum Error {
#[error(transparent)]
Nyquest(#[from] nyquest::Error),
#[error("rate limit")]
RateLimit,
}
impl Error {
pub const NO_RELEASE: Error = Error::Nyquest(nyquest::Error::NonSuccessfulStatusCode(
nyquest::StatusCode::new(404),
));
}
#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Release {
#[serde(rename = "tag_name")]
pub tag: String,
pub name: Option<String>,
#[serde(default)]
pub prerelease: bool,
pub created_at: String,
pub published_at: Option<String>,
pub body: Option<String>,
pub html_url: String,
#[serde(default)]
pub assets: Vec<ReleaseAsset>,
}
impl Release {
fn check_status(status: nyquest::StatusCode, body: &[u8]) -> Result<(), Error> {
if !status.is_successful() {
if status == 403 && body.array_windows::<4>().any(|x| x == b"rate") {
return Err(Error::RateLimit);
}
return Err(Error::Nyquest(nyquest::Error::NonSuccessfulStatusCode(
status,
)));
}
Ok(())
}
pub(crate) fn parse(status: nyquest::StatusCode, body: Vec<u8>) -> Result<Release, Error> {
Self::check_status(status, &body)?;
let release: Release = serde_json::from_slice(&body).map_err(nyquest::Error::from)?;
Ok(release)
}
pub(crate) fn parse_array(
status: nyquest::StatusCode,
body: Vec<u8>,
) -> Result<Vec<Release>, Error> {
Self::check_status(status, &body)?;
let releases: Vec<Release> = serde_json::from_slice(&body).map_err(nyquest::Error::from)?;
Ok(releases)
}
}
#[derive(Debug, Clone, Deserialize)]
#[cfg_attr(test, derive(PartialEq))]
pub struct ReleaseAsset {
pub name: String,
#[serde(rename = "browser_download_url")]
pub download_url: String,
pub size: u64,
pub digest: Option<String>,
#[cfg(feature = "github-extra")]
pub download_count: u64,
}
#[derive(Debug, Clone)]
pub struct UpdateInfo {
current_version: Option<String>,
latest: Release,
has_update: bool,
}
impl UpdateInfo {
pub(crate) fn new(current_version: Option<&str>, latest: Release) -> UpdateInfo {
let has_update = match current_version {
Some(cv) => crate::git::is_newer(cv, &latest.tag),
None => true,
};
UpdateInfo {
current_version: current_version.map(str::to_string),
latest,
has_update,
}
}
pub fn current_version(&self) -> Option<&str> {
self.current_version.as_deref()
}
pub fn latest(&self) -> &Release {
&self.latest
}
#[doc(alias = "update_available")]
#[inline]
pub fn has_update(&self) -> bool {
self.has_update
}
}
#[derive(bon::Builder)]
#[builder(on(String, into))]
pub struct UpdateConfig {
pub(crate) owner: String,
pub(crate) repo: String,
pub(crate) current_version: Option<String>,
#[cfg(feature = "github-extra")]
pub(crate) token: Option<String>,
#[cfg(all(feature = "github-extra", not(feature = "multi-server")))]
pub(crate) base_url: Option<String>,
#[cfg(feature = "multi-server")]
#[builder(default = vec!["https://api.github.com".into()])]
pub(crate) base_urls: Vec<String>,
#[cfg(feature = "github-extra")]
pub(crate) user_agent: Option<String>,
}
#[cfg(all(feature = "github-extra", feature = "multi-server"))]
impl<S: State> UpdateConfigBuilder<S> {
pub fn base_url(self, url: String) -> UpdateConfigBuilder<SetBaseUrls<S>>
where
S::BaseUrls: IsUnset,
{
self.base_urls(vec![url])
}
pub fn maybe_base_url(self, url: Option<String>) -> UpdateConfigBuilder<SetBaseUrls<S>>
where
S::BaseUrls: IsUnset,
{
self.maybe_base_urls(url.map(|s| vec![s]))
}
}
impl UpdateConfig {
#[inline(always)]
pub(crate) fn client_builder(&self) -> impl Iterator<Item = nyquest::ClientBuilder> {
let it = cfg_select! {
feature = "multi-server" => self.base_urls.iter(),
feature = "github-extra" => {{
let base = self.base_url.as_deref().unwrap_or("https://api.github.com");
[base].into_iter()
}}
_ => ["https://api.github.com"].into_iter()
};
let default_ua = concat!("ib-update/", env!("CARGO_PKG_VERSION"));
let ua = cfg_select! {
feature = "github-extra" => self.user_agent.as_deref().unwrap_or(&default_ua),
_ => default_ua,
};
it.map(move |base| {
#[allow(unused_mut)]
let mut builder = nyquest::ClientBuilder::default()
.base_url(base)
.user_agent(ua);
#[cfg(feature = "github-extra")]
if let Some(ref token) = self.token {
builder = builder.with_header("Authorization", format!("Bearer {token}"));
}
builder
})
}
pub(crate) fn releases_latest_path(&self) -> String {
format!("repos/{}/{}/releases/latest", self.owner, self.repo)
}
pub(crate) fn releases_path(&self, per_page: u8) -> String {
format!(
"repos/{}/{}/releases?per_page={per_page}",
self.owner, self.repo
)
}
}
#[cfg(test)]
mod tests {
pub const TEST_OWNER: &str = "Chaoses-Ib";
pub const TEST_REPO: &str = "IbEverythingExt";
}