Skip to main content

soar_dl/
gitlab.rs

1use serde::Deserialize;
2
3use crate::{
4    error::DownloadError,
5    platform::fetch_releases_json,
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 description: Option<String>,
18    pub assets: GitLabAssets,
19}
20
21#[derive(Debug, Clone, Deserialize)]
22pub struct GitLabAssets {
23    pub links: Vec<GitLabAsset>,
24}
25
26#[derive(Debug, Clone, Deserialize)]
27pub struct GitLabAsset {
28    pub name: String,
29    pub direct_asset_url: String,
30}
31
32impl Platform for GitLab {
33    type Release = GitLabRelease;
34
35    const API_BASE: &'static str = "https://gitlab.com";
36    const TOKEN_ENV: [&str; 2] = ["GITLAB_TOKEN", "GL_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_releases_json::<Self::Release>(&path, Self::API_BASE, Self::TOKEN_ENV)
82    }
83}
84
85impl Release for GitLabRelease {
86    type Asset = GitLabAsset;
87
88    /// The release's name
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use soar_dl::gitlab::{GitLabAssets, GitLabRelease};
94    /// use soar_dl::traits::Release;
95    ///
96    /// let r = GitLabRelease {
97    ///     name: "v1.0".into(),
98    ///     tag_name: "v1.0".into(),
99    ///     upcoming_release: false,
100    ///     released_at: "".into(),
101    ///     description: None,
102    ///     assets: GitLabAssets { links: vec![] },
103    /// };
104    /// assert_eq!(r.name(), "v1.0");
105    /// ```
106    fn name(&self) -> &str {
107        &self.name
108    }
109
110    /// Get the release's tag name.
111    ///
112    /// # Examples
113    ///
114    /// ```
115    /// use soar_dl::gitlab::{GitLabAssets, GitLabRelease};
116    /// use soar_dl::traits::Release;
117    ///
118    /// let r = GitLabRelease {
119    ///     name: "Release".into(),
120    ///     tag_name: "v1.0.0".into(),
121    ///     upcoming_release: false,
122    ///     released_at: "2025-01-01T00:00:00Z".into(),
123    ///     description: None,
124    ///     assets: GitLabAssets { links: vec![] },
125    /// };
126    /// assert_eq!(r.tag(), "v1.0.0");
127    /// ```
128    fn tag(&self) -> &str {
129        &self.tag_name
130    }
131
132    /// Indicates whether the release is marked as upcoming.
133    ///
134    /// # Returns
135    ///
136    /// `true` if the release is marked as upcoming, `false` otherwise.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// use soar_dl::gitlab::{GitLabAssets, GitLabRelease};
142    /// use soar_dl::traits::Release;
143    ///
144    /// let rel = GitLabRelease {
145    ///     name: "v1".to_string(),
146    ///     tag_name: "v1".to_string(),
147    ///     upcoming_release: true,
148    ///     released_at: "".to_string(),
149    ///     description: None,
150    ///     assets: GitLabAssets { links: vec![] },
151    /// };
152    /// assert!(rel.is_prerelease());
153    /// ```
154    fn is_prerelease(&self) -> bool {
155        self.upcoming_release
156    }
157
158    /// Get the release's published date/time string.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use soar_dl::gitlab::{GitLabAssets, GitLabRelease};
164    /// use soar_dl::traits::Release;
165    ///
166    /// let r = GitLabRelease {
167    ///     name: String::from("v1"),
168    ///     tag_name: String::from("v1"),
169    ///     upcoming_release: false,
170    ///     released_at: String::from("2020-01-01T00:00:00Z"),
171    ///     description: None,
172    ///     assets: GitLabAssets { links: vec![] },
173    /// };
174    /// assert_eq!(r.published_at(), "2020-01-01T00:00:00Z");
175    /// ```
176    fn published_at(&self) -> &str {
177        &self.released_at
178    }
179
180    /// A slice of assets associated with the release.
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use soar_dl::gitlab::{GitLabAsset, GitLabAssets, GitLabRelease};
186    /// use soar_dl::traits::{Asset, Release};
187    ///
188    /// let asset = GitLabAsset { name: "file.tar.gz".into(), direct_asset_url: "https://example.com/file.tar.gz".into() };
189    /// let assets = GitLabAssets { links: vec![asset.clone()] };
190    /// let release = GitLabRelease {
191    ///     name: "v1.0".into(),
192    ///     tag_name: "v1.0".into(),
193    ///     upcoming_release: false,
194    ///     released_at: "2025-10-31T00:00:00Z".into(),
195    ///     description: None,
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    fn body(&self) -> Option<&str> {
211        self.description.as_deref()
212    }
213}
214
215impl Asset for GitLabAsset {
216    /// Gets the asset's name.
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use soar_dl::gitlab::GitLabAsset;
222    /// use soar_dl::traits::Asset;
223    ///
224    /// let asset = GitLabAsset { name: String::from("v1.0.0"), direct_asset_url: String::from("https://example") };
225    /// assert_eq!(asset.name(), "v1.0.0");
226    /// ```
227    fn name(&self) -> &str {
228        &self.name
229    }
230
231    /// Returns the asset size when available; for GitLab assets this is not provided.
232    ///
233    /// This implementation always reports that size information is unavailable.
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use soar_dl::gitlab::GitLabAsset;
239    /// use soar_dl::traits::Asset;
240    ///
241    /// let asset = GitLabAsset {
242    ///     name: "example".into(),
243    ///     direct_asset_url: "https://gitlab.com/example".into(),
244    /// };
245    /// assert_eq!(asset.size(), None);
246    /// ```
247    fn size(&self) -> Option<u64> {
248        None
249    }
250
251    /// Returns the direct URL of the asset.
252    ///
253    /// # Examples
254    ///
255    /// ```
256    /// use soar_dl::gitlab::GitLabAsset;
257    /// use soar_dl::traits::Asset;
258    ///
259    /// let asset = GitLabAsset {
260    ///     name: String::from("example"),
261    ///     direct_asset_url: String::from("https://example.com/download"),
262    /// };
263    /// assert_eq!(asset.url(), "https://example.com/download");
264    /// ```
265    fn url(&self) -> &str {
266        &self.direct_asset_url
267    }
268}