soar_dl/
gitlab.rs

1use serde::Deserialize;
2
3use crate::{
4    error::DownloadError,
5    platform::fetch_with_fallback,
6    traits::{Asset, Platform, Release},
7};
8
9pub struct GitLab;
10
11#[derive(Debug, Clone, Deserialize)]
12pub struct GitLabRelease {
13    pub name: String,
14    pub tag_name: String,
15    pub upcoming_release: bool,
16    pub released_at: String,
17    pub assets: GitLabAssets,
18}
19
20#[derive(Debug, Clone, Deserialize)]
21pub struct GitLabAssets {
22    pub links: Vec<GitLabAsset>,
23}
24
25#[derive(Debug, Clone, Deserialize)]
26pub struct GitLabAsset {
27    pub name: String,
28    pub direct_asset_url: String,
29}
30
31impl Platform for GitLab {
32    type Release = GitLabRelease;
33
34    const API_PKGFORGE: &'static str = "https://api.gl.pkgforge.dev";
35    const API_UPSTREAM: &'static str = "https://gitlab.com";
36    const TOKEN_ENV: &'static str = "GITLAB_TOKEN";
37
38    /// Fetches releases for a GitLab project, optionally narrowing to a specific tag.
39    ///
40    /// The `project` is the repository identifier (for example `"group/name"` or a numeric project ID).
41    /// If `tag` is provided and the `project` consists only of digits, the fetch targets that single release; otherwise the fetch returns the project's release list.
42    ///
43    /// # Parameters
44    ///
45    /// - `project`: repository identifier or numeric project ID.
46    /// - `tag`: optional release tag to narrow the request.
47    ///
48    /// # Returns
49    ///
50    /// `Ok(Vec<GitLabRelease>)` with the fetched releases on success, or a `DownloadError` on failure.
51    ///
52    /// # Examples
53    ///
54    /// ```no_run
55    /// use soar_dl::gitlab::GitLab;
56    /// use soar_dl::traits::Platform;
57    ///
58    /// // Fetch all releases for a namespaced project
59    /// let _ = GitLab::fetch_releases("group/project", None);
60    ///
61    /// // Fetch a specific release when using a numeric project ID
62    /// let _ = GitLab::fetch_releases("123456", Some("v1.0.0"));
63    /// ```
64    fn fetch_releases(
65        project: &str,
66        tag: Option<&str>,
67    ) -> Result<Vec<Self::Release>, DownloadError> {
68        let encoded_project = project.replace('/', "%2F");
69        let path = match tag {
70            Some(t) if project.chars().all(char::is_numeric) => {
71                let encoded_tag =
72                    url::form_urlencoded::byte_serialize(t.as_bytes()).collect::<String>();
73                format!(
74                    "/api/v4/projects/{}/releases/{}",
75                    encoded_project, encoded_tag
76                )
77            }
78            _ => format!("/api/v4/projects/{}/releases", encoded_project),
79        };
80
81        fetch_with_fallback::<Self::Release>(
82            &path,
83            Self::API_UPSTREAM,
84            Self::API_PKGFORGE,
85            Self::TOKEN_ENV,
86        )
87    }
88}
89
90impl Release for GitLabRelease {
91    type Asset = GitLabAsset;
92
93    /// The release's name
94    ///
95    /// # Examples
96    ///
97    /// ```
98    /// use soar_dl::gitlab::{GitLabAssets, GitLabRelease};
99    /// use soar_dl::traits::Release;
100    ///
101    /// let r = GitLabRelease {
102    ///     name: "v1.0".into(),
103    ///     tag_name: "v1.0".into(),
104    ///     upcoming_release: false,
105    ///     released_at: "".into(),
106    ///     assets: GitLabAssets { links: vec![] },
107    /// };
108    /// assert_eq!(r.name(), "v1.0");
109    /// ```
110    fn name(&self) -> &str {
111        &self.name
112    }
113
114    /// Get the release's tag name.
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// use soar_dl::gitlab::{GitLabAssets, GitLabRelease};
120    /// use soar_dl::traits::Release;
121    ///
122    /// let r = GitLabRelease {
123    ///     name: "Release".into(),
124    ///     tag_name: "v1.0.0".into(),
125    ///     upcoming_release: false,
126    ///     released_at: "2025-01-01T00:00:00Z".into(),
127    ///     assets: GitLabAssets { links: vec![] },
128    /// };
129    /// assert_eq!(r.tag(), "v1.0.0");
130    /// ```
131    fn tag(&self) -> &str {
132        &self.tag_name
133    }
134
135    /// Indicates whether the release is marked as upcoming.
136    ///
137    /// # Returns
138    ///
139    /// `true` if the release is marked as upcoming, `false` otherwise.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use soar_dl::gitlab::{GitLabAssets, GitLabRelease};
145    /// use soar_dl::traits::Release;
146    ///
147    /// let rel = GitLabRelease {
148    ///     name: "v1".to_string(),
149    ///     tag_name: "v1".to_string(),
150    ///     upcoming_release: true,
151    ///     released_at: "".to_string(),
152    ///     assets: GitLabAssets { links: vec![] },
153    /// };
154    /// assert!(rel.is_prerelease());
155    /// ```
156    fn is_prerelease(&self) -> bool {
157        self.upcoming_release
158    }
159
160    /// Get the release's published date/time string.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use soar_dl::gitlab::{GitLabAssets, GitLabRelease};
166    /// use soar_dl::traits::Release;
167    ///
168    /// let r = GitLabRelease {
169    ///     name: String::from("v1"),
170    ///     tag_name: String::from("v1"),
171    ///     upcoming_release: false,
172    ///     released_at: String::from("2020-01-01T00:00:00Z"),
173    ///     assets: GitLabAssets { links: vec![] },
174    /// };
175    /// assert_eq!(r.published_at(), "2020-01-01T00:00:00Z");
176    /// ```
177    fn published_at(&self) -> &str {
178        &self.released_at
179    }
180
181    /// A slice of assets associated with the release.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// use soar_dl::gitlab::{GitLabAsset, GitLabAssets, GitLabRelease};
187    /// use soar_dl::traits::{Asset, Release};
188    ///
189    /// let asset = GitLabAsset { name: "file.tar.gz".into(), direct_asset_url: "https://example.com/file.tar.gz".into() };
190    /// let assets = GitLabAssets { links: vec![asset.clone()] };
191    /// let release = GitLabRelease {
192    ///     name: "v1.0".into(),
193    ///     tag_name: "v1.0".into(),
194    ///     upcoming_release: false,
195    ///     released_at: "2025-10-31T00:00:00Z".into(),
196    ///     assets,
197    /// };
198    /// let slice = release.assets();
199    /// assert_eq!(slice.len(), 1);
200    /// assert_eq!(slice[0].name(), "file.tar.gz");
201    /// ```
202    ///
203    /// # Returns
204    ///
205    /// A slice of the release's assets.
206    fn assets(&self) -> &[Self::Asset] {
207        &self.assets.links
208    }
209}
210
211impl Asset for GitLabAsset {
212    /// Gets the asset's name.
213    ///
214    /// # Examples
215    ///
216    /// ```
217    /// use soar_dl::gitlab::GitLabAsset;
218    /// use soar_dl::traits::Asset;
219    ///
220    /// let asset = GitLabAsset { name: String::from("v1.0.0"), direct_asset_url: String::from("https://example") };
221    /// assert_eq!(asset.name(), "v1.0.0");
222    /// ```
223    fn name(&self) -> &str {
224        &self.name
225    }
226
227    /// Returns the asset size when available; for GitLab assets this is not provided.
228    ///
229    /// This implementation always reports that size information is unavailable.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use soar_dl::gitlab::GitLabAsset;
235    /// use soar_dl::traits::Asset;
236    ///
237    /// let asset = GitLabAsset {
238    ///     name: "example".into(),
239    ///     direct_asset_url: "https://gitlab.com/example".into(),
240    /// };
241    /// assert_eq!(asset.size(), None);
242    /// ```
243    fn size(&self) -> Option<u64> {
244        None
245    }
246
247    /// Returns the direct URL of the asset.
248    ///
249    /// # Examples
250    ///
251    /// ```
252    /// use soar_dl::gitlab::GitLabAsset;
253    /// use soar_dl::traits::Asset;
254    ///
255    /// let asset = GitLabAsset {
256    ///     name: String::from("example"),
257    ///     direct_asset_url: String::from("https://example.com/download"),
258    /// };
259    /// assert_eq!(asset.url(), "https://example.com/download");
260    /// ```
261    fn url(&self) -> &str {
262        &self.direct_asset_url
263    }
264}