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