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}