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}