Skip to main content

upstream_rs/providers/gitlab/
gitlab_adapter.rs

1use 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        // Convert asset links to Assets
74        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, // GitLab doesn't provide size in link metadata
84                created_at,
85            ));
86        }
87
88        // Convert source archives to Assets
89        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, // GitLab doesn't have numeric release IDs
107            tag: dto.tag_name,
108            name: dto.name,
109            body: dto.description,
110            is_draft: false, // GitLab doesn't have draft releases
111            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}