1use std::{cmp::Ordering, fmt::Display};
2
3use serde::Deserialize;
4
5use crate::{error::Error, ReleaseAsset};
6
7use super::{DownloadApiTrait, SimpleTag};
8
9#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Clone)]
10pub struct GithubAsset {
11 pub name: String,
12 pub url: String,
13}
14
15impl Display for GithubAsset {
16 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17 writeln!(f, "Name: {}", self.name)?;
18 writeln!(f, "Name: {}", self.url)
19 }
20}
21
22impl ReleaseAsset for GithubAsset {
23 fn get_name(&self) -> &str {
24 &self.name
25 }
26
27 fn get_download_url(&self) -> &str {
28 &self.url
29 }
30
31 fn download(
32 &self,
33 additional_headers: Vec<(&str, &str)>,
34 download_callback: Option<impl Fn(f32)>,
35 ) -> Result<(), Error> {
36 crate::download(self, additional_headers, download_callback)
37 }
38}
39
40#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Clone)]
41pub struct GithubRelease {
42 pub tag_name: String,
43 pub target_commitish: String,
44 pub name: String,
45 pub prerelease: bool,
46 pub assets: Vec<GithubAsset>,
47 pub body: String,
48}
49
50impl Display for GithubRelease {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 writeln!(f, "Tag: {}", self.tag_name)?;
53 writeln!(f, "Branch: {}", self.target_commitish)?;
54 writeln!(f, "Name: {}", self.name)?;
55 writeln!(f, "Prerelease: {}", self.prerelease)?;
56 writeln!(f, "Assets:")?;
57 for asset in &self.assets {
58 writeln!(f, "{}", asset)?;
59 }
60
61 Ok(())
62 }
63}
64
65#[derive(Debug, PartialEq, Eq, Hash)]
66pub struct GithubApi {
67 api_url: Option<String>,
68 owner: String,
69 repo: String,
70 auth_token: Option<String>,
71 branch: Option<String>,
72 prerelease: bool,
73 specific_tag: Option<String>,
74 current_version: Option<String>,
75 asset_name: Option<String>,
76}
77
78impl GithubApi {
79 pub fn new(owner: &str, repo: &str) -> Self {
80 GithubApi {
81 api_url: None,
82 owner: owner.to_string(),
83 repo: repo.to_string(),
84 auth_token: None,
85 branch: None,
86 prerelease: false,
87 specific_tag: None,
88 current_version: None,
89 asset_name: None,
90 }
91 }
92
93 pub fn api_url(mut self, api_url: &str) -> Self {
95 self.api_url = Some(api_url.to_string());
96 self
97 }
98
99 pub fn auth_token(mut self, auth_token: &str) -> Self {
101 self.auth_token = Some(format!("auth {auth_token}"));
102 self
103 }
104
105 pub fn branch(mut self, branch: &str) -> Self {
107 self.branch = Some(branch.to_string());
108 self
109 }
110
111 pub fn prerelease(mut self, prerelease: bool) -> Self {
113 self.prerelease = prerelease;
114 self
115 }
116
117 pub fn specific_tag(mut self, specific_tag: &str) -> Self {
119 self.specific_tag = Some(specific_tag.to_string());
120 self
121 }
122
123 pub fn current_version(mut self, current_version: &str) -> Self {
125 self.current_version = Some(current_version.to_string());
126 self
127 }
128
129 pub fn asset_name(mut self, asset_name: &str) -> Self {
131 self.asset_name = Some(asset_name.to_string());
132 self
133 }
134
135 fn get_releases(&self, per_page: i32, page: i32) -> Result<Vec<GithubRelease>, Error> {
136 let api_url = format!(
137 "https://{}/repos/{}/{}/releases?per_page={}&page={}",
138 self.api_url.as_deref().unwrap_or("api.github.com"),
139 self.owner,
140 self.repo,
141 per_page,
142 page
143 );
144
145 let mut request = ureq::get(&api_url).set("user-agent", "rust-ureq/updater");
146 if let Some(token) = &self.auth_token {
147 request = request.set("authorization", token);
148 }
149
150 let release_list: Vec<GithubRelease> = request.call()?.into_json()?;
151 Ok(release_list)
152 }
153
154 fn filter_release(&self, e: &GithubRelease) -> bool {
155 if !self.prerelease && e.prerelease {
156 return false;
157 }
158 let specific_tag = match self.specific_tag {
159 Some(ref tag) => *tag == e.tag_name,
160 None => true,
161 };
162
163 let branch = match self.branch {
164 Some(ref branch) => *branch == e.target_commitish,
165 None => true,
166 };
167
168 let asset_name = match self.asset_name {
169 Some(ref asset_name) => e.assets.iter().any(|e| e.name == *asset_name),
170 None => true,
171 };
172
173 specific_tag && branch && asset_name
174 }
175
176 fn match_releases<'releases>(
177 &self,
178 releases: &'releases [GithubRelease],
179 ) -> Vec<&'releases GithubRelease> {
180 releases.iter().filter(|e| self.filter_release(e)).collect()
181 }
182
183 pub fn send(
185 &self,
186 sort_func: Option<&impl Fn(&str, &str) -> Ordering>,
187 ) -> Result<GithubRelease, Error> {
188 let mut releases = self.get_releases(100, 1)?;
189
190 let mut page = 3;
191 let mut new_releases = self.get_releases(100, 2)?;
192 while !new_releases.is_empty() {
193 releases.extend(new_releases);
194 new_releases = self.get_releases(100, page)?;
195 page += 1;
196 }
197
198 let mut matching = self.match_releases(&releases);
199 if matching.is_empty() {
200 return Err(Error::NoRelease);
201 }
202
203 match sort_func {
204 Some(sort_func) => {
205 matching.sort_by(|a, b| sort_func(&a.tag_name, &b.tag_name));
206 }
207 None => matching.sort_by(|a, b| SimpleTag::simple_compare(&a.tag_name, &b.tag_name)),
208 };
209
210 let latest_release = matching.last().ok_or(Error::NoRelease)?;
211 Ok((*latest_release).clone())
212 }
213
214 pub fn get_latest(
216 &self,
217 sort_func: Option<&impl Fn(&str, &str) -> Ordering>,
218 ) -> Result<GithubRelease, Error> {
219 self.send(sort_func)
220 }
221
222 pub fn get_all(&self) -> Result<Vec<GithubRelease>, Error> {
225 let mut releases = Vec::new();
226 let mut page = 1;
227 loop {
228 let fetched_releases = self.get_releases(100, page)?;
229 if fetched_releases.is_empty() {
230 break;
231 }
232
233 releases.extend(fetched_releases);
234 page += 1;
235 }
236
237 Ok(releases
238 .into_iter()
239 .filter(|e| self.filter_release(e))
240 .collect::<Vec<_>>())
241 }
242
243 pub fn get_newer(
247 &self,
248 sort_func: Option<&impl Fn(&str, &str) -> Ordering>,
249 ) -> Result<Option<GithubRelease>, Error> {
250 let latest_release = self.send(sort_func)?;
251 let is_newer = match self.current_version {
252 Some(ref current_version) => {
253 let newer = match sort_func {
254 Some(sort_func) => sort_func(&latest_release.tag_name, current_version),
255 None => SimpleTag::simple_compare(&latest_release.tag_name, current_version),
256 };
257
258 newer == Ordering::Greater
259 }
260 None => true,
261 };
262
263 if is_newer {
264 Ok(Some(latest_release))
265 } else {
266 Ok(None)
267 }
268 }
269}
270
271impl DownloadApiTrait for GithubApi {
272 fn download<Asset: ReleaseAsset>(
273 &self,
274 asset: &Asset,
275 download_callback: Option<impl Fn(f32)>,
276 ) -> Result<(), Error> {
277 let mut headers = Vec::new();
278
279 if let Some(token) = self.auth_token.as_deref() {
280 headers.push(("authorization", token));
281 }
282
283 asset.download(headers, download_callback)
284 }
285}