Skip to main content

soar_dl/
github.rs

1use serde::Deserialize;
2
3use crate::{
4    error::DownloadError,
5    platform::fetch_with_fallback,
6    traits::{Asset, Platform, Release},
7};
8
9pub struct Github;
10
11#[derive(Debug, Clone, Deserialize)]
12pub struct GithubRelease {
13    pub name: Option<String>,
14    pub tag_name: String,
15    pub prerelease: bool,
16    pub published_at: String,
17    pub body: Option<String>,
18    pub assets: Vec<GithubAsset>,
19}
20
21#[derive(Debug, Clone, Deserialize)]
22pub struct GithubAsset {
23    pub name: String,
24    pub size: u64,
25    pub browser_download_url: String,
26}
27
28impl Platform for Github {
29    type Release = GithubRelease;
30
31    const API_PKGFORGE: &'static str = "https://api.gh.pkgforge.dev";
32    const API_UPSTREAM: &'static str = "https://api.github.com";
33    const TOKEN_ENV: [&str; 2] = ["GITHUB_TOKEN", "GH_TOKEN"];
34
35    /// Fetches releases for the given GitHub repository, optionally filtered by a specific tag.
36    ///
37    /// If `tag` is provided, fetches the release that matches that tag; otherwise fetches the repository's releases (up to 100 per page).
38    ///
39    /// # Arguments
40    ///
41    /// * `project` — repository identifier in the form "owner/repo".
42    /// * `tag` — optional release tag to filter the results.
43    ///
44    /// # Returns
45    ///
46    /// `Ok` with a vector of releases on success, or `Err(DownloadError)` on failure.
47    ///
48    /// # Examples
49    ///
50    /// ```no_run
51    /// use soar_dl::github::Github;
52    /// use soar_dl::traits::{Platform, Release};
53    ///
54    /// let releases = Github::fetch_releases("rust-lang/rust", None).unwrap();
55    /// assert!(releases.iter().all(|r| r.tag().len() > 0));
56    /// ```
57    fn fetch_releases(
58        project: &str,
59        tag: Option<&str>,
60    ) -> Result<Vec<Self::Release>, DownloadError> {
61        let path = match tag {
62            Some(tag) => {
63                let encoded_tag =
64                    url::form_urlencoded::byte_serialize(tag.as_bytes()).collect::<String>();
65                format!(
66                    "/repos/{project}/releases/tags/{}?per_page=100",
67                    encoded_tag
68                )
69            }
70            None => format!("/repos/{project}/releases?per_page=100"),
71        };
72
73        fetch_with_fallback::<Self::Release>(
74            &path,
75            Self::API_UPSTREAM,
76            Self::API_PKGFORGE,
77            Self::TOKEN_ENV,
78        )
79    }
80}
81
82impl Release for GithubRelease {
83    type Asset = GithubAsset;
84
85    /// The release's name, or an empty string if the release has no name.
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use soar_dl::github::GithubRelease;
91    /// use soar_dl::traits::Release;
92    ///
93    /// let r = GithubRelease {
94    ///     name: Some("v1.0".into()),
95    ///     tag_name: "v1.0".into(),
96    ///     prerelease: false,
97    ///     published_at: "".into(),
98    ///     body: None,
99    ///     assets: vec![],
100    /// };
101    /// assert_eq!(r.name(), "v1.0");
102    ///
103    /// let unnamed = GithubRelease {
104    ///     name: None,
105    ///     tag_name: "v1.1".into(),
106    ///     prerelease: false,
107    ///     published_at: "".into(),
108    ///     body: None,
109    ///     assets: vec![],
110    /// };
111    /// assert_eq!(unnamed.name(), "");
112    /// ```
113    fn name(&self) -> &str {
114        self.name.as_deref().unwrap_or("")
115    }
116
117    /// Get the release tag as a string slice.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use soar_dl::github::GithubRelease;
123    /// use soar_dl::traits::Release;
124    ///
125    /// let release = GithubRelease {
126    ///     name: None,
127    ///     tag_name: "v1.0.0".into(),
128    ///     prerelease: false,
129    ///     published_at: "".into(),
130    ///     body: None,
131    ///     assets: vec![],
132    /// };
133    /// assert_eq!(release.tag(), "v1.0.0");
134    /// ```
135    ///
136    /// # Returns
137    ///
138    /// `&str` containing the release tag.
139    fn tag(&self) -> &str {
140        &self.tag_name
141    }
142
143    /// Indicates whether the release is marked as a prerelease.
144    ///
145    /// # Returns
146    ///
147    /// `true` if the release is marked as a prerelease, `false` otherwise.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use soar_dl::github::GithubRelease;
153    /// use soar_dl::traits::Release;
154    ///
155    /// let r = GithubRelease {
156    ///     name: None,
157    ///     tag_name: "v1.0.0".to_string(),
158    ///     prerelease: true,
159    ///     published_at: "".to_string(),
160    ///     body: None,
161    ///     assets: vec![],
162    /// };
163    /// assert!(r.is_prerelease());
164    /// ```
165    fn is_prerelease(&self) -> bool {
166        self.prerelease
167    }
168
169    /// Returns the release's publication timestamp as an RFC 3339 formatted string.
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use soar_dl::github::GithubRelease;
175    /// use soar_dl::traits::Release;
176    ///
177    /// let r = GithubRelease {
178    ///     name: None,
179    ///     tag_name: "v1.0.0".into(),
180    ///     prerelease: false,
181    ///     published_at: "2021-01-01T00:00:00Z".into(),
182    ///     body: None,
183    ///     assets: vec![],
184    /// };
185    /// assert_eq!(r.published_at(), "2021-01-01T00:00:00Z");
186    /// ```
187    fn published_at(&self) -> &str {
188        &self.published_at
189    }
190
191    /// Get a slice of assets associated with the release.
192    ///
193    /// The slice contains the release's assets in declaration order.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// use soar_dl::github::{GithubRelease, GithubAsset};
199    /// use soar_dl::traits::Release;
200    ///
201    /// let asset = GithubAsset {
202    ///     name: "example.zip".into(),
203    ///     size: 1024,
204    ///     browser_download_url: "https://example.com/example.zip".into(),
205    /// };
206    ///
207    /// let release = GithubRelease {
208    ///     name: Some("v1.0".into()),
209    ///     tag_name: "v1.0".into(),
210    ///     prerelease: false,
211    ///     published_at: "2025-01-01T00:00:00Z".into(),
212    ///     body: None,
213    ///     assets: vec![asset],
214    /// };
215    ///
216    /// assert_eq!(release.assets().len(), 1);
217    /// ```
218    fn assets(&self) -> &[Self::Asset] {
219        &self.assets
220    }
221
222    fn body(&self) -> Option<&str> {
223        self.body.as_deref()
224    }
225}
226
227impl Asset for GithubAsset {
228    /// Retrieves the asset's name.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use soar_dl::github::GithubAsset;
234    /// use soar_dl::traits::Asset;
235    ///
236    /// let asset = GithubAsset {
237    ///     name: "file.zip".to_string(),
238    ///     size: 123,
239    ///     browser_download_url: "https://example.com/file.zip".to_string(),
240    /// };
241    /// assert_eq!(asset.name(), "file.zip");
242    /// ```
243    ///
244    /// # Returns
245    ///
246    /// A `&str` containing the asset's name.
247    fn name(&self) -> &str {
248        &self.name
249    }
250
251    /// Asset size in bytes.
252    ///
253    /// # Returns
254    ///
255    /// `Some(size)` containing the asset size in bytes.
256    ///
257    /// # Examples
258    ///
259    /// ```
260    /// use soar_dl::github::GithubAsset;
261    /// use soar_dl::traits::Asset;
262    ///
263    /// let asset = GithubAsset { name: "file".into(), size: 12345, browser_download_url: "https://example.com".into() };
264    /// assert_eq!(asset.size(), Some(12345));
265    /// ```
266    fn size(&self) -> Option<u64> {
267        Some(self.size)
268    }
269
270    /// Returns the asset's browser download URL.
271    ///
272    /// # Examples
273    ///
274    /// ```
275    /// use soar_dl::github::GithubAsset;
276    /// use soar_dl::traits::Asset;
277    ///
278    /// let asset = GithubAsset {
279    ///     name: "example".into(),
280    ///     size: 123,
281    ///     browser_download_url: "https://example.com/download".into(),
282    /// };
283    /// assert_eq!(asset.url(), "https://example.com/download");
284    /// ```
285    fn url(&self) -> &str {
286        &self.browser_download_url
287    }
288}