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};
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 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, created_at,
80 ));
81 }
82
83 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, tag: dto.tag_name,
103 name: dto.name,
104 body: dto.description,
105 is_draft: false, 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}