upstream_rs/providers/github/
github_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::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}