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