Skip to main content

upstream_rs/providers/github/
github_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::github_client::GithubClient;
9use super::github_dtos::{GithubAssetDto, GithubReleaseDto};
10
11#[derive(Debug, Clone)]
12pub struct GithubAdapter {
13    client: GithubClient,
14}
15
16impl GithubAdapter {
17    pub fn new(client: GithubClient) -> 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, slug: &str, tag: &str) -> Result<Release> {
36        let dto = self.client.get_release_by_tag(slug, tag).await?;
37        Ok(self.convert_release(dto))
38    }
39
40    pub async fn get_latest_release(&self, slug: &str) -> Result<Release> {
41        let dto = self.client.get_latest_release(slug).await?;
42        Ok(self.convert_release(dto))
43    }
44
45    pub async fn get_releases(
46        &self,
47        slug: &str,
48        per_page: Option<u32>,
49        max_total: Option<u32>,
50    ) -> Result<Vec<Release>> {
51        let dtos = self.client.get_releases(slug, per_page, max_total).await?;
52        Ok(dtos
53            .into_iter()
54            .map(|dto| self.convert_release(dto))
55            .collect())
56    }
57
58    fn convert_asset(dto: GithubAssetDto) -> Asset {
59        let created_at = Self::parse_timestamp(&dto.created_at);
60        Asset::new(
61            dto.browser_download_url,
62            dto.id as u64,
63            dto.name,
64            dto.size as u64,
65            created_at,
66        )
67    }
68
69    fn convert_release(&self, dto: GithubReleaseDto) -> Release {
70        let assets: Vec<Asset> = dto.assets.into_iter().map(Self::convert_asset).collect();
71        let version =
72            Version::from_tag(&dto.tag_name).unwrap_or_else(|_| Version::new(0, 0, 0, false));
73        Release {
74            id: dto.id as u64,
75            tag: dto.tag_name,
76            name: dto.name,
77            body: dto.body,
78            is_draft: dto.draft,
79            is_prerelease: dto.prerelease,
80            published_at: Self::parse_timestamp(&dto.published_at),
81            assets,
82            version,
83        }
84    }
85
86    fn parse_timestamp(raw: &str) -> DateTime<Utc> {
87        if raw.trim().is_empty() {
88            return DateTime::<Utc>::MIN_UTC;
89        }
90        raw.parse::<DateTime<Utc>>()
91            .unwrap_or(DateTime::<Utc>::MIN_UTC)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::GithubAdapter;
98    use crate::providers::github::github_client::GithubClient;
99    use crate::providers::github::github_dtos::{GithubAssetDto, GithubReleaseDto};
100
101    #[test]
102    fn parse_timestamp_returns_min_for_invalid_or_empty_values() {
103        assert_eq!(
104            GithubAdapter::parse_timestamp(""),
105            chrono::DateTime::<chrono::Utc>::MIN_UTC
106        );
107        assert_eq!(
108            GithubAdapter::parse_timestamp("not-a-date"),
109            chrono::DateTime::<chrono::Utc>::MIN_UTC
110        );
111    }
112
113    #[test]
114    fn convert_release_maps_assets_and_version() {
115        let adapter = GithubAdapter::new(GithubClient::new(None).expect("github client"));
116        let dto = GithubReleaseDto {
117            id: 12,
118            tag_name: "v2.3.4".to_string(),
119            name: "Release 2.3.4".to_string(),
120            body: "notes".to_string(),
121            prerelease: true,
122            draft: false,
123            published_at: "2026-02-21T00:00:00Z".to_string(),
124            assets: vec![GithubAssetDto {
125                id: 9,
126                name: "tool-linux-x86_64.tar.gz".to_string(),
127                browser_download_url: "https://example.invalid/tool-linux-x86_64.tar.gz"
128                    .to_string(),
129                size: 123,
130                content_type: "application/gzip".to_string(),
131                created_at: "2026-02-20T00:00:00Z".to_string(),
132            }],
133        };
134
135        let release = adapter.convert_release(dto);
136        assert_eq!(release.id, 12);
137        assert_eq!(release.version.to_string(), "2.3.4");
138        assert!(release.is_prerelease);
139        assert_eq!(release.assets.len(), 1);
140        assert_eq!(release.assets[0].id, 9);
141    }
142}