upstream_rs/providers/gitlab/
gitlab_adapter.rs1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use std::path::Path;
4
5use crate::models::common::Version;
6use crate::models::provider::{Asset, Release};
7use crate::providers::release_provider::ReleaseProvider;
8
9use super::gitlab_client::GitlabClient;
10use super::gitlab_dtos::GitlabReleaseDto;
11
12#[derive(Debug, Clone)]
13pub struct GitlabAdapter {
14 client: GitlabClient,
15}
16
17impl GitlabAdapter {
18 pub fn new(client: GitlabClient) -> Self {
19 Self { client }
20 }
21
22 pub async fn download_asset<F>(
23 &self,
24 asset: &Asset,
25 destination_path: &Path,
26 dl_callback: &mut Option<F>,
27 ) -> Result<()>
28 where
29 F: FnMut(u64, u64),
30 {
31 self.client
32 .download_file(&asset.download_url, destination_path, dl_callback)
33 .await
34 }
35
36 pub async fn get_release_by_tag(&self, project_path: &str, tag: &str) -> Result<Release> {
37 let dto = self.client.get_release_by_tag(project_path, tag).await?;
38 Ok(self.convert_release(dto))
39 }
40
41 pub async fn get_latest_release(&self, project_path: &str) -> Result<Release> {
42 let releases = self.get_releases(project_path, Some(1), Some(1)).await?;
43 releases
44 .into_iter()
45 .next()
46 .ok_or_else(|| anyhow::anyhow!("No releases found for project {}", project_path))
47 }
48
49 pub async fn get_releases(
50 &self,
51 project_path: &str,
52 per_page: Option<u32>,
53 max_total: Option<u32>,
54 ) -> Result<Vec<Release>> {
55 let dtos = self
56 .client
57 .get_releases(project_path, per_page, max_total)
58 .await?;
59 Ok(dtos
60 .into_iter()
61 .map(|dto| self.convert_release(dto))
62 .collect())
63 }
64
65 pub async fn get_branch_head_sha(&self, project_path: &str, branch: &str) -> Result<String> {
66 self.client.get_branch_head_sha(project_path, branch).await
67 }
68
69 fn convert_release(&self, dto: GitlabReleaseDto) -> Release {
70 let mut assets = Vec::new();
71 let mut asset_id: u64 = 0;
72
73 for link in dto.assets.links {
75 asset_id += 1;
76 let download_url = link.direct_asset_url.unwrap_or(link.url);
77 let created_at = Self::parse_timestamp(&dto.created_at);
78
79 assets.push(Asset::new(
80 download_url,
81 asset_id,
82 link.name,
83 0, created_at,
85 ));
86 }
87
88 for source in dto.assets.sources {
90 asset_id += 1;
91 let name = format!("source.{}", source.format);
92 let created_at = Self::parse_timestamp(&dto.created_at);
93
94 assets.push(Asset::new(source.url, asset_id, name, 0, created_at));
95 }
96
97 let version =
98 Version::from_tag(&dto.tag_name).unwrap_or_else(|_| Version::new(0, 0, 0, false));
99 let published_at = dto
100 .released_at
101 .as_ref()
102 .map(|s| Self::parse_timestamp(s))
103 .unwrap_or_else(|| Self::parse_timestamp(&dto.created_at));
104
105 Release {
106 id: asset_id, tag: dto.tag_name,
108 name: dto.name,
109 body: dto.description,
110 is_draft: false, is_prerelease: dto.upcoming_release.unwrap_or(false),
112 published_at,
113 assets,
114 version,
115 }
116 }
117
118 fn parse_timestamp(raw: &str) -> DateTime<Utc> {
119 if raw.trim().is_empty() {
120 return DateTime::<Utc>::MIN_UTC;
121 }
122 raw.parse::<DateTime<Utc>>()
123 .unwrap_or(DateTime::<Utc>::MIN_UTC)
124 }
125}
126
127#[async_trait::async_trait(?Send)]
128impl ReleaseProvider for GitlabAdapter {
129 async fn get_latest_release(&self, slug: &str) -> Result<Release> {
130 GitlabAdapter::get_latest_release(self, slug).await
131 }
132
133 async fn get_releases(
134 &self,
135 slug: &str,
136 per_page: Option<u32>,
137 max_total: Option<u32>,
138 ) -> Result<Vec<Release>> {
139 GitlabAdapter::get_releases(self, slug, per_page, max_total).await
140 }
141
142 async fn get_release_by_tag(&self, slug: &str, tag: &str) -> Result<Release> {
143 GitlabAdapter::get_release_by_tag(self, slug, tag).await
144 }
145
146 async fn get_branch_head_sha(&self, slug: &str, branch: &str) -> Result<String> {
147 GitlabAdapter::get_branch_head_sha(self, slug, branch).await
148 }
149
150 async fn download_asset(
151 &self,
152 asset: &Asset,
153 destination_path: &Path,
154 dl_callback: Option<&mut (dyn FnMut(u64, u64) + '_)>,
155 ) -> Result<()> {
156 let mut forwarded = dl_callback;
157 GitlabAdapter::download_asset(self, asset, destination_path, &mut forwarded).await
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::GitlabAdapter;
164 use crate::providers::gitlab::gitlab_client::GitlabClient;
165 use crate::providers::gitlab::gitlab_dtos::{
166 GitlabAssetsDto, GitlabLinkDto, GitlabReleaseDto, GitlabSourceDto,
167 };
168
169 #[test]
170 fn parse_timestamp_handles_invalid_values() {
171 assert_eq!(
172 GitlabAdapter::parse_timestamp(""),
173 chrono::DateTime::<chrono::Utc>::MIN_UTC
174 );
175 assert_eq!(
176 GitlabAdapter::parse_timestamp("bad-date"),
177 chrono::DateTime::<chrono::Utc>::MIN_UTC
178 );
179 }
180
181 #[test]
182 fn convert_release_combines_links_and_sources_into_assets() {
183 let adapter = GitlabAdapter::new(GitlabClient::new(None, None).expect("gitlab client"));
184 let dto = GitlabReleaseDto {
185 tag_name: "v1.9.0".to_string(),
186 name: "v1.9.0".to_string(),
187 description: "notes".to_string(),
188 created_at: "2026-02-21T00:00:00Z".to_string(),
189 released_at: None,
190 upcoming_release: Some(false),
191 assets: GitlabAssetsDto {
192 count: 2,
193 links: vec![GitlabLinkDto {
194 id: 1,
195 name: "tool-linux.tar.gz".to_string(),
196 url: "https://example.invalid/tool-linux.tar.gz".to_string(),
197 direct_asset_url: None,
198 link_type: None,
199 }],
200 sources: vec![GitlabSourceDto {
201 format: "tar.gz".to_string(),
202 url: "https://example.invalid/source.tar.gz".to_string(),
203 }],
204 },
205 };
206
207 let release = adapter.convert_release(dto);
208 assert_eq!(release.version.to_string(), "1.9.0");
209 assert_eq!(release.assets.len(), 2);
210 assert_eq!(release.assets[1].name, "source.tar.gz");
211 }
212}